From 1b0ea78e4099d4622e5f00e3b24755185954c02c Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:29:40 -0500 Subject: [PATCH 1/2] refactor: extract status and step enums from magic strings (#951) Co-authored-by: Wendong-Fan Co-authored-by: Claude --- .../BrowserAgentWorkSpace/index.tsx | 15 +- src/components/ChatBox/BottomBox/index.tsx | 3 +- src/components/ChatBox/FloatingAction.tsx | 5 +- src/components/ChatBox/ProjectSection.tsx | 3 +- src/components/ChatBox/TaskBox/TaskCard.tsx | 101 ++++---- src/components/ChatBox/UserQueryGroup.tsx | 15 +- src/components/ChatBox/index.tsx | 59 ++--- .../GroupedHistoryView/ProjectGroup.tsx | 3 +- .../GroupedHistoryView/TaskItem.tsx | 3 +- src/components/HistorySidebar/index.tsx | 3 +- src/components/TopBar/index.tsx | 9 +- src/components/WorkFlow/node.tsx | 109 +++++---- src/pages/Dashboard/Project.tsx | 5 +- src/pages/Home.tsx | 3 +- src/store/chatStore.ts | 230 ++++++++++-------- src/store/projectStore.ts | 3 +- src/types/chatbox.d.ts | 12 +- src/types/constants.ts | 97 ++++++++ 18 files changed, 414 insertions(+), 264 deletions(-) create mode 100644 src/types/constants.ts diff --git a/src/components/BrowserAgentWorkSpace/index.tsx b/src/components/BrowserAgentWorkSpace/index.tsx index d4a17dfb..47cad6f2 100644 --- a/src/components/BrowserAgentWorkSpace/index.tsx +++ b/src/components/BrowserAgentWorkSpace/index.tsx @@ -29,6 +29,7 @@ import { import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { TaskState } from '../TaskState'; import { Button } from '../ui/button'; +import { TaskStatus } from "@/types/constants"; export default function Home() { //Get Chatstore for the active project's task @@ -223,26 +224,26 @@ export default function Home() { } done={ activeAgent?.tasks?.filter( - (task) => task.status === 'completed' + (task) => task.status === TaskStatus.COMPLETED ).length || 0 } progress={ activeAgent?.tasks?.filter( (task) => - task.status !== 'failed' && - task.status !== 'completed' && - task.status !== 'skipped' && - task.status !== 'waiting' + task.status !== TaskStatus.FAILED && + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.SKIPPED && + task.status !== TaskStatus.WAITING ).length || 0 } failed={ - activeAgent?.tasks?.filter((task) => task.status === 'failed') + activeAgent?.tasks?.filter((task) => task.status === TaskStatus.FAILED) .length || 0 } skipped={ activeAgent?.tasks?.filter( (task) => - task.status === 'skipped' || task.status === 'waiting' + task.status === TaskStatus.SKIPPED || task.status === TaskStatus.WAITING ).length || 0 } /> diff --git a/src/components/ChatBox/BottomBox/index.tsx b/src/components/ChatBox/BottomBox/index.tsx index b6da8593..30eace95 100644 --- a/src/components/ChatBox/BottomBox/index.tsx +++ b/src/components/ChatBox/BottomBox/index.tsx @@ -16,6 +16,7 @@ import { BoxAction } from './BoxAction'; import { BoxHeaderConfirm, BoxHeaderSplitting } from './BoxHeader'; import { FileAttachment, Inputbox, InputboxProps } from './InputBox'; import { QueuedBox, QueuedMessage } from './QueuedBox'; +import { type ChatTaskStatusType } from "@/types/constants"; export type BottomBoxState = | 'input' @@ -42,7 +43,7 @@ interface BottomBoxProps { // Task info tokens?: number; taskTime?: string; - taskStatus?: 'running' | 'finished' | 'pending' | 'pause'; + taskStatus?: ChatTaskStatusType; // Replay onReplay?: () => void; diff --git a/src/components/ChatBox/FloatingAction.tsx b/src/components/ChatBox/FloatingAction.tsx index a6406f30..a78a273c 100644 --- a/src/components/ChatBox/FloatingAction.tsx +++ b/src/components/ChatBox/FloatingAction.tsx @@ -14,10 +14,11 @@ import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; +import { ChatTaskStatus, type ChatTaskStatusType } from '@/types/constants'; export interface FloatingActionProps { /** Current task status */ - status: 'running' | 'pause' | 'pending' | 'finished'; + status: ChatTaskStatusType; /** Callback when pause button is clicked */ // onPause?: () => void; // Commented out - temporary not needed /** Callback when resume button is clicked */ @@ -39,7 +40,7 @@ export const FloatingAction = ({ className, }: FloatingActionProps) => { // Only show when task is running (removed pause state) - if (status !== 'running') { + if (status !== ChatTaskStatus.RUNNING) { return null; } diff --git a/src/components/ChatBox/ProjectSection.tsx b/src/components/ChatBox/ProjectSection.tsx index 6c1ded50..b5737c53 100644 --- a/src/components/ChatBox/ProjectSection.tsx +++ b/src/components/ChatBox/ProjectSection.tsx @@ -17,6 +17,7 @@ import { motion } from 'framer-motion'; import React from 'react'; import { FloatingAction } from './FloatingAction'; import { UserQueryGroup } from './UserQueryGroup'; +import { AgentStep } from '@/types/constants'; interface ProjectSectionProps { chatId: string; @@ -161,7 +162,7 @@ function groupMessagesByQuery(messages: any[]) { userMessage: message, otherMessages: [], }; - } else if (message.step === 'to_sub_tasks') { + } else if (message.step === AgentStep.TO_SUB_TASKS) { // Task planning message - each should get its own panel // Skip if we've already processed this to_sub_tasks diff --git a/src/components/ChatBox/TaskBox/TaskCard.tsx b/src/components/ChatBox/TaskBox/TaskCard.tsx index cac56e25..1230deb2 100644 --- a/src/components/ChatBox/TaskBox/TaskCard.tsx +++ b/src/components/ChatBox/TaskBox/TaskCard.tsx @@ -37,6 +37,7 @@ import { import { useMemo, useState, useRef, useEffect } from "react"; import { TaskState, TaskStateType } from "@/components/TaskState"; import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; +import { TaskStatus, ChatTaskStatus } from "@/types/constants"; interface TaskCardProps { taskInfo: any[]; @@ -87,24 +88,24 @@ export function TaskCard({ const newFiltered = tasks.filter((task) => { switch (selectedState) { case "done": - return task.status === "completed" && !task.reAssignTo; + return task.status === TaskStatus.COMPLETED && !task.reAssignTo; case "ongoing": return ( - task.status !== "failed" && - task.status !== "completed" && - task.status !== "skipped" && - task.status !== "waiting" && - task.status !== "" + task.status !== TaskStatus.FAILED && + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.SKIPPED && + task.status !== TaskStatus.WAITING && + task.status !== TaskStatus.EMPTY ); case "pending": return ( - (task.status === "skipped" || - task.status === "waiting" || - task.status === "") && + (task.status === TaskStatus.SKIPPED || + task.status === TaskStatus.WAITING || + task.status === TaskStatus.EMPTY) && !task.reAssignTo ); case "failed": - return task.status === "failed"; + return task.status === TaskStatus.FAILED; default: return false; } @@ -115,7 +116,7 @@ export function TaskCard({ const isAllTaskFinished = useMemo(() => { return ( - chatStore.tasks[chatStore.activeTaskId as string].status === "finished" + chatStore.tasks[chatStore.activeTaskId as string].status === ChatTaskStatus.FINISHED ); }, [chatStore.tasks[chatStore.activeTaskId as string].status]); @@ -207,32 +208,32 @@ export function TaskCard({ done={ taskInfo.filter( (task) => - task.content !== "" && task.status === "completed" + task.content !== "" && task.status === TaskStatus.COMPLETED ).length || 0 } progress={ taskInfo.filter( (task) => task.content !== "" && - task.status !== "completed" && - task.status !== "failed" && - task.status !== "skipped" && - task.status !== "waiting" && - task.status !== "" + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.FAILED && + task.status !== TaskStatus.SKIPPED && + task.status !== TaskStatus.WAITING && + task.status !== TaskStatus.EMPTY ).length || 0 } skipped={ taskInfo.filter( (task) => task.content !== "" && - (task.status === "skipped" || - task.status === "waiting" || - task.status === "") + (task.status === TaskStatus.SKIPPED || + task.status === TaskStatus.WAITING || + task.status === TaskStatus.EMPTY) ).length || 0 } failed={ taskInfo.filter( - (task) => task.content !== "" && task.status === "failed" + (task) => task.content !== "" && task.status === TaskStatus.FAILED ).length || 0 } forceVisible={true} @@ -243,29 +244,29 @@ export function TaskCard({ task.status === "completed") + taskRunning?.filter((task) => task.status === TaskStatus.COMPLETED) .length || 0 } progress={ taskRunning?.filter( (task) => - task.status !== "completed" && - task.status !== "failed" && - task.status !== "skipped" && - task.status !== "waiting" && - task.status !== "" + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.FAILED && + task.status !== TaskStatus.SKIPPED && + task.status !== TaskStatus.WAITING && + task.status !== TaskStatus.EMPTY ).length || 0 } skipped={ taskRunning?.filter( (task) => - task.status === "skipped" || - task.status === "waiting" || - task.status === "" + task.status === TaskStatus.SKIPPED || + task.status === TaskStatus.WAITING || + task.status === TaskStatus.EMPTY ).length || 0 } failed={ - taskRunning?.filter((task) => task.status === "failed") + taskRunning?.filter((task) => task.status === TaskStatus.FAILED) .length || 0 } forceVisible={true} @@ -288,8 +289,8 @@ export function TaskCard({
{taskRunning?.filter( (task) => - task.status === "completed" || - task.status === "failed" + task.status === TaskStatus.COMPLETED || + task.status === TaskStatus.FAILED ).length || 0} /{taskRunning?.length || 0}
@@ -371,70 +372,70 @@ export function TaskCard({ } }} key={`taskList-${task.id}`} - className={`rounded-lg flex gap-2 py-sm px-sm transition-all duration-300 ease-in-out animate-in fade-in-0 slide-in-from-left-2 ${task.status === "completed" + className={`rounded-lg flex gap-2 py-sm px-sm transition-all duration-300 ease-in-out animate-in fade-in-0 slide-in-from-left-2 ${task.status === TaskStatus.COMPLETED ? "bg-task-fill-success" - : task.status === "failed" + : task.status === TaskStatus.FAILED ? "bg-task-fill-error" - : task.status === "running" + : task.status === TaskStatus.RUNNING ? "bg-task-fill-running" - : task.status === "blocked" + : task.status === TaskStatus.BLOCKED ? "bg-task-fill-warning" : "bg-task-fill-running" - } border border-solid border-transparent cursor-pointer ${task.status === "completed" + } border border-solid border-transparent cursor-pointer ${task.status === TaskStatus.COMPLETED ? "hover:border-bg-fill-success-primary" - : task.status === "failed" + : task.status === TaskStatus.FAILED ? "hover:border-task-border-focus-error" - : task.status === "running" + : task.status === TaskStatus.RUNNING ? "hover:border-border-primary" - : task.status === "blocked" + : task.status === TaskStatus.BLOCKED ? "hover:border-task-border-focus-warning" : "border-transparent" } `} >
- {task.status === "running" && ( + {task.status === TaskStatus.RUNNING && ( )} - {task.status === "skipped" && ( + {task.status === TaskStatus.SKIPPED && ( )} - {task.status === "completed" && ( + {task.status === TaskStatus.COMPLETED && ( )} - {task.status === "failed" && ( + {task.status === TaskStatus.FAILED && ( )} - {task.status === "blocked" && ( + {task.status === TaskStatus.BLOCKED && ( )} - {task.status === "" && ( + {task.status === TaskStatus.EMPTY && ( )}
= ({ if (userMessageIndex > 0) { // Check the previous message - if it's an agent message with step 'ask', this is a human-reply const prevMessage = messages[userMessageIndex - 1]; - return prevMessage?.role === 'agent' && prevMessage?.step === 'ask'; + return prevMessage?.role === 'agent' && prevMessage?.step === AgentStep.ASK; } return false; })()); @@ -102,7 +103,7 @@ export const UserQueryGroup: React.FC = ({ .filter((m: any) => m.role === 'user') .pop()?.id && // Only show during active phases (not finished) - chatState.tasks[activeTaskId].status !== 'finished'; + chatState.tasks[activeTaskId].status !== ChatTaskStatus.FINISHED; // Only show the fallback task box for the newest query while the agent is still splitting work. // Simple Q&A sessions set hasWaitComfirm to true, so we should not render an empty task box there. @@ -185,11 +186,11 @@ export const UserQueryGroup: React.FC = ({ // Check if we're in skeleton phase const anyToSubTasksMessage = task?.messages.find( - (m: any) => m.step === 'to_sub_tasks' + (m: any) => m.step === AgentStep.TO_SUB_TASKS ); const isSkeletonPhase = task && - ((task.status !== 'finished' && + ((task.status !== ChatTaskStatus.FINISHED && !anyToSubTasksMessage && !task.hasWaitComfirm && task.messages.length > 0) || @@ -282,7 +283,7 @@ export const UserQueryGroup: React.FC = ({ {/* Other Messages */} {queryGroup.otherMessages.map((message) => { if (message.content.length > 0) { - if (message.step === 'end') { + if (message.step === AgentStep.END) { return ( = ({ ); } - } else if (message.step === 'end' && message.content === '') { + } else if (message.step === AgentStep.END && message.content === '') { return ( = ({ // Notice Card if ( - message.step === 'notice_card' && + message.step === AgentStep.NOTICE_CARD && !task?.isTakeControl && task?.cotList && task.cotList.length > 0 diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index e2bbdc24..2c172496 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -30,6 +30,7 @@ import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import BottomBox from './BottomBox'; import { ProjectChatContainer } from './ProjectChatContainer'; +import { AgentStep, ChatTaskStatus } from '@/types/constants'; export default function ChatBox(): JSX.Element { const [message, setMessage] = useState(''); @@ -191,7 +192,7 @@ export default function ChatBox(): JSX.Element { if (!chatStore) return; const _hasSubTask = chatStore.tasks[ chatStore.activeTaskId as string - ]?.messages?.find((message) => message.step === 'to_sub_tasks') + ]?.messages?.find((message) => message.step === AgentStep.TO_SUB_TASKS) ? true : false; setHasSubTask(_hasSubTask); @@ -253,12 +254,12 @@ export default function ChatBox(): JSX.Element { const task = chatStore.tasks[chatStore.activeTaskId]; return ( // running or paused - task.status === 'running' || - task.status === 'pause' || + task.status === ChatTaskStatus.RUNNING || + task.status === ChatTaskStatus.PAUSE || // splitting phase - task.messages.some((m) => m.step === 'to_sub_tasks' && !m.isConfirm) || + task.messages.some((m) => m.step === AgentStep.TO_SUB_TASKS && !m.isConfirm) || // skeleton/computing phase - (!task.messages.find((m) => m.step === 'to_sub_tasks') && + (!task.messages.find((m) => m.step === AgentStep.TO_SUB_TASKS) && !task.hasWaitComfirm && task.messages.length > 0) || task.isTakeControl @@ -424,17 +425,17 @@ export default function ChatBox(): JSX.Element { const requiresHumanReply = Boolean(task?.activeAsk); const isTaskBusy = // running or paused counts as busy - (task.status === 'running' && task.hasMessages) || - task.status === 'pause' || + (task.status === ChatTaskStatus.RUNNING && task.hasMessages) || + task.status === ChatTaskStatus.PAUSE || // splitting phase: has to_sub_tasks not confirmed OR skeleton computing - task.messages.some((m) => m.step === 'to_sub_tasks' && !m.isConfirm) || - (!task.messages.find((m) => m.step === 'to_sub_tasks') && + task.messages.some((m) => m.step === AgentStep.TO_SUB_TASKS && !m.isConfirm) || + (!task.messages.find((m) => m.step === AgentStep.TO_SUB_TASKS) && !task.hasWaitComfirm && task.messages.length > 0) || task.isTakeControl || // explicit confirm wait while task is pending but card not confirmed yet - (!!task.messages.find((m) => m.step === 'to_sub_tasks' && !m.isConfirm) && - task.status === 'pending'); + (!!task.messages.find((m) => m.step === AgentStep.TO_SUB_TASKS && !m.isConfirm) && + task.status === ChatTaskStatus.PENDING); const isReplayChatStore = task?.type === 'replay'; if (!requiresHumanReply && isTaskBusy && !isReplayChatStore) { toast.error( @@ -489,7 +490,7 @@ export default function ChatBox(): JSX.Element { const hasMessages = chatStore.tasks[_taskId as string].messages.length > 0; const isFinished = - chatStore.tasks[_taskId as string].status === 'finished'; + chatStore.tasks[_taskId as string].status === ChatTaskStatus.FINISHED; const hasWaitComfirm = chatStore.tasks[_taskId as string]?.hasWaitComfirm; @@ -497,7 +498,7 @@ export default function ChatBox(): JSX.Element { const wasTaskStopped = isFinished && !chatStore.tasks[_taskId as string].messages.some( - (m) => m.step === 'end' // Natural completion has an "end" step message + (m) => m.step === AgentStep.END // Natural completion has an "end" step message ); // Continue conversation if: @@ -508,16 +509,16 @@ export default function ChatBox(): JSX.Element { (hasWaitComfirm && !wasTaskStopped) || (isFinished && !wasTaskStopped) || (hasMessages && - chatStore.tasks[_taskId as string].status === 'pending'); + chatStore.tasks[_taskId as string].status === ChatTaskStatus.PENDING); if (shouldContinueConversation) { // Check if this is the very first message and task hasn't started const hasSimpleResponse = chatStore.tasks[ _taskId as string - ].messages.some((m) => m.step === 'wait_confirm'); + ].messages.some((m) => m.step === AgentStep.WAIT_CONFIRM); const hasComplexTask = chatStore.tasks[ _taskId as string - ].messages.some((m) => m.step === 'to_sub_tasks'); + ].messages.some((m) => m.step === AgentStep.TO_SUB_TASKS); const hasErrorMessage = chatStore.tasks[ _taskId as string ].messages.some( @@ -527,7 +528,7 @@ export default function ChatBox(): JSX.Element { // Only start a new task if: pending, no messages processed yet // OR while or after replaying a project if ( - (chatStore.tasks[_taskId as string].status === 'pending' && + (chatStore.tasks[_taskId as string].status === ChatTaskStatus.PENDING && !hasSimpleResponse && !hasComplexTask && !isFinished) || @@ -692,7 +693,7 @@ export default function ChatBox(): JSX.Element { const handlePauseResume = () => { const taskId = chatStore.activeTaskId as string; const task = chatStore.tasks[taskId]; - const type = task.status === 'running' ? 'pause' : 'resume'; + const type = task.status === ChatTaskStatus.RUNNING ? 'pause' : 'resume'; setIsPauseResumeLoading(true); if (type === 'pause') { @@ -701,10 +702,10 @@ export default function ChatBox(): JSX.Element { elapsed += now - taskTime; chatStore.setElapsed(taskId, elapsed); chatStore.setTaskTime(taskId, 0); - chatStore.setStatus(taskId, 'pause'); + chatStore.setStatus(taskId, ChatTaskStatus.PAUSE); } else { chatStore.setTaskTime(taskId, Date.now()); - chatStore.setStatus(taskId, 'running'); + chatStore.setStatus(taskId, ChatTaskStatus.RUNNING); } fetchPut(`/task/${projectStore.activeProjectId}/take-control`, { @@ -802,7 +803,7 @@ export default function ChatBox(): JSX.Element { // Get question and attachments before any deletions const messageIndex = chatStore.tasks[taskId].messages.findLastIndex( - (item) => item.step === 'to_sub_tasks' + (item) => item.step === AgentStep.TO_SUB_TASKS ); const questionMessage = chatStore.tasks[taskId].messages[messageIndex - 2]; const question = questionMessage.content; @@ -856,16 +857,16 @@ export default function ChatBox(): JSX.Element { // Check for any to_sub_tasks message (confirmed or not) const anyToSubTasksMessage = task.messages.find( - (m) => m.step === 'to_sub_tasks' + (m) => m.step === AgentStep.TO_SUB_TASKS ); const toSubTasksMessage = task.messages.find( - (m) => m.step === 'to_sub_tasks' && !m.isConfirm + (m) => m.step === AgentStep.TO_SUB_TASKS && !m.isConfirm ); // Determine if we're in the "splitting in progress" phase (skeleton visible) // Only show splitting if there's NO to_sub_tasks message yet (not even confirmed) const isSkeletonPhase = - (task.status !== 'finished' && + (task.status !== ChatTaskStatus.FINISHED && !anyToSubTasksMessage && !task.hasWaitComfirm && task.messages.length > 0) || @@ -879,7 +880,7 @@ export default function ChatBox(): JSX.Element { if ( toSubTasksMessage && !toSubTasksMessage.isConfirm && - task.status === 'pending' + task.status === ChatTaskStatus.PENDING ) { return 'confirm'; } @@ -890,11 +891,11 @@ export default function ChatBox(): JSX.Element { } // Check task status - if (task.status === 'running' || task.status === 'pause') { + if (task.status === ChatTaskStatus.RUNNING || task.status === ChatTaskStatus.PAUSE) { return 'running'; } - if (task.status === 'finished' && task.type !== '') { + if (task.status === ChatTaskStatus.FINISHED && task.type !== '') { return 'finished'; } @@ -938,7 +939,7 @@ export default function ChatBox(): JSX.Element { // Note: Replay creates a new chatstore, so no conflicts const task = chatStore.tasks[chatStore.activeTaskId as string]; // Only skip backend call if task is finished or hasn't started yet (no messages) - if (task && task.messages.length > 0 && task.status !== 'finished') { + if (task && task.messages.length > 0 && task.status !== ChatTaskStatus.FINISHED) { try { await fetchDelete(`/chat/${project_id}/remove-task/${task_id}`, { project_id: project_id, @@ -1010,7 +1011,7 @@ export default function ChatBox(): JSX.Element { taskStatus={chatStore.tasks[chatStore.activeTaskId]?.status} onReplay={handleReplay} replayDisabled={ - chatStore.tasks[chatStore.activeTaskId]?.status !== 'finished' + chatStore.tasks[chatStore.activeTaskId]?.status !== ChatTaskStatus.FINISHED } replayLoading={isReplayLoading} onPauseResume={handlePauseResume} diff --git a/src/components/GroupedHistoryView/ProjectGroup.tsx b/src/components/GroupedHistoryView/ProjectGroup.tsx index c16bdd92..af45eab8 100644 --- a/src/components/GroupedHistoryView/ProjectGroup.tsx +++ b/src/components/GroupedHistoryView/ProjectGroup.tsx @@ -25,6 +25,7 @@ import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { replayProject } from '@/lib/replay'; import { useProjectStore } from '@/store/projectStore'; import { ProjectGroup as ProjectGroupType } from '@/types/history'; +import { ChatTaskStatus } from "@/types/constants"; import { motion } from 'framer-motion'; import { Edit, @@ -100,7 +101,7 @@ export default function ProjectGroup({ // Check if any task in chatStore with matching task_id has pending status return Object.entries(chatStore.tasks).some( ([taskId, task]) => - projectTaskIds.includes(taskId) && task.status === 'pending' + projectTaskIds.includes(taskId) && task.status === ChatTaskStatus.PENDING ); }, [chatStore?.tasks, project.tasks]); const _hasIssue = hasHumanInLoop; diff --git a/src/components/GroupedHistoryView/TaskItem.tsx b/src/components/GroupedHistoryView/TaskItem.tsx index 3e2bd900..c5978478 100644 --- a/src/components/GroupedHistoryView/TaskItem.tsx +++ b/src/components/GroupedHistoryView/TaskItem.tsx @@ -22,6 +22,7 @@ import { import { Tag } from '@/components/ui/tag'; import { TooltipSimple } from '@/components/ui/tooltip'; import { HistoryTask } from '@/types/history'; +import { ChatTaskStatus } from '@/types/constants'; import { CheckCircle, CirclePause, @@ -63,7 +64,7 @@ export default function TaskItem({ const { t } = useTranslation(); // Check if task is paused (for ongoing tasks) - const isPaused = (task as any)._taskData?.status === 'pause'; + const isPaused = (task as any)._taskData?.status === ChatTaskStatus.PAUSE; const getStatusTag = (status: number) => { // ChatStatus enum: ongoing = 1, done = 2 diff --git a/src/components/HistorySidebar/index.tsx b/src/components/HistorySidebar/index.tsx index e8a05991..a31dd516 100644 --- a/src/components/HistorySidebar/index.tsx +++ b/src/components/HistorySidebar/index.tsx @@ -45,6 +45,7 @@ import { import { Tag } from '../ui/tag'; import { TooltipSimple } from '../ui/tooltip'; import SearchInput from './SearchInput'; +import { ChatTaskStatus } from "@/types/constants"; export default function HistorySidebar() { const { t } = useTranslation(); @@ -90,7 +91,7 @@ export default function HistorySidebar() { Object.keys(csState.tasks || {}).forEach((taskId) => { const task = csState.tasks[taskId]; // Only include ongoing tasks - if (task.status !== 'finished' && !task.type) { + if (task.status !== ChatTaskStatus.FINISHED && !task.type) { hasOngoingTasks = true; taskCount++; if (task.tokens) { diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index 31990a53..3a2fb871 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -45,6 +45,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; +import { ChatTaskStatus } from '@/types/constants'; function HeaderWin() { const { t } = useTranslation(); @@ -144,7 +145,7 @@ function HeaderWin() { const task = chatStore.tasks[taskId]; // Stop the task if it's running - if (task && task.status === 'running') { + if (task && task.status === ChatTaskStatus.RUNNING) { await fetchPut(`/task/${taskId}/take-control`, { action: 'stop', }); @@ -158,7 +159,7 @@ function HeaderWin() { } // Delete from history using historyId - if (historyId && task.status !== 'finished') { + if (historyId && task.status !== ChatTaskStatus.FINISHED) { try { await proxyFetchDelete(`/api/chat/history/${historyId}`); // Remove from local store @@ -333,7 +334,7 @@ function HeaderWin() { chatStore.tasks[chatStore.activeTaskId as string] ?.hasMessages || chatStore.tasks[chatStore.activeTaskId as string]?.status !== - 'pending') && ( + ChatTaskStatus.PENDING) && ( { switch (selectedState) { case "done": - return task.status === "completed" && !task.reAssignTo; + return task.status === TaskStatus.COMPLETED && !task.reAssignTo; case "reassigned": return !!task.reAssignTo; case "ongoing": return ( - task.status !== "failed" && - task.status !== "completed" && - task.status !== "skipped" && - task.status !== "waiting" && - task.status !== "" && + task.status !== TaskStatus.FAILED && + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.SKIPPED && + task.status !== TaskStatus.WAITING && + task.status !== TaskStatus.EMPTY && !task.reAssignTo ); case "pending": return ( - (task.status === "skipped" || - task.status === "waiting" || - task.status === "") && + (task.status === TaskStatus.SKIPPED || + task.status === TaskStatus.WAITING || + task.status === TaskStatus.EMPTY) && !task.reAssignTo ); case "failed": - return task.status === "failed"; + return task.status === TaskStatus.FAILED; default: return false; } @@ -136,13 +137,13 @@ export function Node({ id, data }: NodeProps) { // Find running task with active toolkits const runningTaskWithToolkits = tasks.find( (task) => - task.status === "running" && + task.status === TaskStatus.RUNNING && task.toolkits && task.toolkits.length > 0 ); // Reset tracking when no tasks are running - const hasRunningTasks = tasks.some((task) => task.status === "running"); + const hasRunningTasks = tasks.some((task) => task.status === TaskStatus.RUNNING); if (!hasRunningTasks && lastAutoExpandedTaskIdRef.current) { lastAutoExpandedTaskIdRef.current = null; } @@ -164,8 +165,8 @@ export function Node({ id, data }: NodeProps) { data.agent?.tasks, // Add specific dependencies that actually change data.agent?.tasks?.length, - data.agent?.tasks?.find((t) => t.status === "running")?.id, - data.agent?.tasks?.find((t) => t.status === "running")?.toolkits?.length, + data.agent?.tasks?.find((t) => t.status === TaskStatus.RUNNING)?.id, + data.agent?.tasks?.find((t) => t.status === TaskStatus.RUNNING)?.toolkits?.length, id, data.onExpandChange, isExpanded, @@ -198,7 +199,7 @@ export function Node({ id, data }: NodeProps) { const handleShowLog = () => { if (!isExpanded) { setSelectedTask( - data.agent?.tasks.find((task) => task.status === "running") || + data.agent?.tasks.find((task) => task.status === TaskStatus.RUNNING) || data.agent?.tasks[0] ); } @@ -318,7 +319,7 @@ export function Node({ id, data }: NodeProps) { borderColor: "border-bg-fill-multimodal-active", bgColorLight: "bg-fuchsia-200", }, - social_medium_agent: { + social_media_agent: { name: "Social Media Agent", icon: , textColor: "text-purple-700", @@ -549,7 +550,7 @@ export function Node({ id, data }: NodeProps) { all={data.agent.tasks?.length || 0} done={ data.agent?.tasks?.filter( - (task) => task.status === "completed" && !task.reAssignTo + (task) => task.status === TaskStatus.COMPLETED && !task.reAssignTo ).length || 0 } reAssignTo={ @@ -559,26 +560,26 @@ export function Node({ id, data }: NodeProps) { progress={ data.agent?.tasks?.filter( (task) => - task.status !== "failed" && - task.status !== "completed" && - task.status !== "skipped" && - task.status !== "waiting" && - task.status !== "" && + task.status !== TaskStatus.FAILED && + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.SKIPPED && + task.status !== TaskStatus.WAITING && + task.status !== TaskStatus.EMPTY && !task.reAssignTo ).length || 0 } skipped={ data.agent?.tasks?.filter( (task) => - (task.status === "skipped" || - task.status === "waiting" || - task.status === "") && + (task.status === TaskStatus.SKIPPED || + task.status === TaskStatus.WAITING || + task.status === TaskStatus.EMPTY) && !task.reAssignTo ).length || 0 } failed={ data.agent?.tasks?.filter( - (task) => task.status === "failed" + (task) => task.status === TaskStatus.FAILED ).length || 0 } selectedState={selectedState} @@ -625,32 +626,32 @@ export function Node({ id, data }: NodeProps) { key={`taskList-${task.id}-${task.failure_count}`} className={`rounded-lg flex gap-2 py-sm px-sm transition-all duration-300 ease-in-out animate-in fade-in-0 slide-in-from-left-2 ${task.reAssignTo ? "bg-task-fill-warning" - : task.status === "completed" + : task.status === TaskStatus.COMPLETED ? "bg-task-fill-success" - : task.status === "failed" + : task.status === TaskStatus.FAILED ? "bg-task-fill-error" - : task.status === "running" + : task.status === TaskStatus.RUNNING ? "bg-task-fill-running" - : task.status === "blocked" + : task.status === TaskStatus.BLOCKED ? "bg-task-fill-warning" : "bg-task-fill-running" - } border border-solid border-transparent cursor-pointer ${task.status === "completed" + } border border-solid border-transparent cursor-pointer ${task.status === TaskStatus.COMPLETED ? "hover:border-bg-fill-success-primary" - : task.status === "failed" + : task.status === TaskStatus.FAILED ? "hover:border-task-border-focus-error" - : task.status === "running" + : task.status === TaskStatus.RUNNING ? "hover:border-border-primary" - : task.status === "blocked" + : task.status === TaskStatus.BLOCKED ? "hover:border-task-border-focus-warning" : "border-transparent" } ${selectedTask?.id === task.id - ? task.status === "completed" + ? task.status === TaskStatus.COMPLETED ? "!border-bg-fill-success-primary" - : task.status === "failed" + : task.status === TaskStatus.FAILED ? "!border-text-cuation-primary" - : task.status === "running" + : task.status === TaskStatus.RUNNING ? "!border-border-primary" - : task.status === "blocked" + : task.status === TaskStatus.BLOCKED ? "!border-text-warning-primary" : "border-transparent" : "border-transparent" @@ -663,41 +664,41 @@ export function Node({ id, data }: NodeProps) { ) : ( // normal task <> - {task.status === "running" && ( + {task.status === TaskStatus.RUNNING && ( )} - {task.status === "skipped" && ( + {task.status === TaskStatus.SKIPPED && ( )} - {task.status === "completed" && ( + {task.status === TaskStatus.COMPLETED && ( )} - {task.status === "failed" && ( + {task.status === TaskStatus.FAILED && ( )} - {task.status === "blocked" && ( + {task.status === TaskStatus.BLOCKED && ( )} - {(task.status === "" || - task.status === "waiting") && ( + {(task.status === TaskStatus.EMPTY || + task.status === TaskStatus.WAITING) && ( )} @@ -705,9 +706,9 @@ export function Node({ id, data }: NodeProps) {
0 && (
{task.content}
- {task?.status === "running" && ( + {task?.status === TaskStatus.RUNNING && (
{/* active toolkit */} {task.toolkits && @@ -746,7 +747,7 @@ export function Node({ id, data }: NodeProps) { .filter( (tool: any) => tool.toolkitName !== "notice" ) - .at(-1)?.toolkitStatus === "running" && ( + .at(-1)?.toolkitStatus === AgentStatusValue.RUNNING && (
{agentMap[data.type]?.icon ?? ( @@ -830,12 +831,12 @@ export function Node({ id, data }: NodeProps) { > {/* {toolkit.toolkitStatus} */}
- {toolkit.toolkitStatus === "running" ? ( + {toolkit.toolkitStatus === AgentStatusValue.RUNNING ? ( ) : ( diff --git a/src/pages/Dashboard/Project.tsx b/src/pages/Dashboard/Project.tsx index 42572c7d..8838cd47 100644 --- a/src/pages/Dashboard/Project.tsx +++ b/src/pages/Dashboard/Project.tsx @@ -22,6 +22,7 @@ import { Bird, CodeXml, FileText, Globe, Image } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import { ChatTaskStatus } from '@/types/constants'; export default function Project() { const { t } = useTranslation(); @@ -214,9 +215,9 @@ export default function Project() { action: type, }); if (type === 'pause') { - chatStore.setStatus(taskId, 'pause'); + chatStore.setStatus(taskId, ChatTaskStatus.PAUSE); } else { - chatStore.setStatus(taskId, 'running'); + chatStore.setStatus(taskId, ChatTaskStatus.RUNNING); } }; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 432da001..37fed258 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -12,6 +12,7 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import { ChatTaskStatus } from "@/types/constants"; import ChatBox from "@/components/ChatBox"; import Workflow from "@/components/WorkFlow"; import Folder from "@/components/Folder"; @@ -88,7 +89,7 @@ export default function Home() { // capture webview const captureWebview = async () => { const activeTask = chatStore.tasks[chatStore.activeTaskId as string]; - if (!activeTask || activeTask.status === "finished") { + if (!activeTask || activeTask.status === ChatTaskStatus.FINISHED) { return; } webviews.map((webview) => { diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 15a7fc0f..bcd84458 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -26,6 +26,14 @@ import { import { showCreditsToast } from '@/components/Toast/creditsToast'; import { showStorageToast } from '@/components/Toast/storageToast'; import { generateUniqueId, uploadLog } from '@/lib'; +import { + AgentMessageStatus, + AgentStatusValue, + AgentStep, + ChatTaskStatus, + TaskStatus, + type ChatTaskStatusType, +} from '@/types/constants'; import { fetchEventSource } from '@microsoft/fetch-event-source'; import { FileText } from 'lucide-react'; import { toast } from 'sonner'; @@ -50,7 +58,7 @@ interface Task { activeWorkSpace: string | null; hasMessages: boolean; activeAgent: string; - status: 'running' | 'finished' | 'pending' | 'pause'; + status: ChatTaskStatusType; taskTime: number; elapsed: number; tokens: number; @@ -77,10 +85,7 @@ export interface ChatStore { create: (id?: string, type?: any) => string; removeTask: (taskId: string) => void; stopTask: (taskId: string) => void; - setStatus: ( - taskId: string, - status: 'running' | 'finished' | 'pending' | 'pause' - ) => void; + setStatus: (taskId: string, status: ChatTaskStatusType) => void; setActiveTaskId: (taskId: string) => void; replay: (taskId: string, question: string, time: number) => Promise; startTask: ( @@ -251,7 +256,7 @@ const chatStore = (initial?: Partial) => activeWorkSpace: 'workflow', hasMessages: false, activeAgent: '', - status: 'pending', + status: ChatTaskStatus.PENDING, taskTime: 0, tokens: 0, elapsed: 0, @@ -275,7 +280,9 @@ const chatStore = (initial?: Partial) => const { tasks, setProgressValue, activeTaskId } = get(); const taskRunning = [...tasks[taskId].taskRunning]; const finishedTask = taskRunning?.filter( - (task) => task.status === 'completed' || task.status === 'failed' + (task) => + task.status === TaskStatus.COMPLETED || + task.status === TaskStatus.FAILED ).length; const taskProgress = ( ((finishedTask || 0) / (taskRunning?.length || 0)) * @@ -380,7 +387,7 @@ const chatStore = (initial?: Partial) => ...state.tasks, [taskId]: { ...state.tasks[taskId], - status: 'finished', + status: ChatTaskStatus.FINISHED, }, }, }; @@ -752,11 +759,12 @@ const chatStore = (initial?: Partial) => // - Task switching: confirmed, new_task_state, end // - Multi-turn simple answer: wait_confirm const isTaskSwitchingEvent = - agentMessages.step === 'confirmed' || - agentMessages.step === 'new_task_state' || - agentMessages.step === 'end'; + agentMessages.step === AgentStep.CONFIRMED || + agentMessages.step === AgentStep.NEW_TASK_STATE || + agentMessages.step === AgentStep.END; - const isMultiTurnSimpleAnswer = agentMessages.step === 'wait_confirm'; + const isMultiTurnSimpleAnswer = + agentMessages.step === AgentStep.WAIT_CONFIRM; if (!currentTask) { console.log( @@ -766,7 +774,7 @@ const chatStore = (initial?: Partial) => } if ( - currentTask.status === 'finished' && + currentTask.status === ChatTaskStatus.FINISHED && !isTaskSwitchingEvent && !isMultiTurnSimpleAnswer ) { @@ -795,7 +803,7 @@ const chatStore = (initial?: Partial) => */ let currentTaskId = getCurrentTaskId(); const previousChatStore = getCurrentChatStore(); - if (agentMessages.step === 'confirmed') { + if (agentMessages.step === AgentStep.CONFIRMED) { const { question } = agentMessages.data; const shouldCreateNewChat = project_id && (question || messageContent); @@ -890,7 +898,10 @@ const chatStore = (initial?: Partial) => } else { //NOTE: Triggered only with first "confirmed" in the project //Handle Original cases - with old chatStore - previousChatStore.setStatus(currentTaskId, 'pending'); + previousChatStore.setStatus( + currentTaskId, + ChatTaskStatus.PENDING + ); previousChatStore.setHasWaitComfirm(currentTaskId, false); } @@ -940,8 +951,8 @@ const chatStore = (initial?: Partial) => } = getCurrentChatStore(); currentTaskId = getCurrentTaskId(); - // if (tasks[currentTaskId].status === 'finished') return - if (agentMessages.step === 'decompose_text') { + // if (tasks[currentTaskId].status === ChatTaskStatus.FINISHED) return + if (agentMessages.step === AgentStep.DECOMPOSE_TEXT) { const { content } = agentMessages.data; const text = content; const currentId = getCurrentTaskId(); @@ -992,7 +1003,7 @@ const chatStore = (initial?: Partial) => return; } - if (agentMessages.step === 'to_sub_tasks') { + if (agentMessages.step === AgentStep.TO_SUB_TASKS) { // Clear streaming decompose text when task splitting is done clearStreamingDecomposeText(currentTaskId); // Clean up TTFT tracking @@ -1001,18 +1012,20 @@ const chatStore = (initial?: Partial) => // Check if task is already confirmed - don't overwrite user edits const existingToSubTasksMessage = tasks[ currentTaskId - ].messages.findLast((m: Message) => m.step === 'to_sub_tasks'); + ].messages.findLast( + (m: Message) => m.step === AgentStep.TO_SUB_TASKS + ); if (existingToSubTasksMessage?.isConfirm) { return; } // Check if this is a multi-turn scenario after task completion const isMultiTurnAfterCompletion = - tasks[currentTaskId].status === 'finished'; + tasks[currentTaskId].status === ChatTaskStatus.FINISHED; // Reset status for multi-turn complex tasks to allow splitting panel to show if (isMultiTurnAfterCompletion) { - setStatus(currentTaskId, 'pending'); + setStatus(currentTaskId, ChatTaskStatus.PENDING); } // Each splitting round starts in a clean editing state @@ -1020,7 +1033,7 @@ const chatStore = (initial?: Partial) => const messages = [...tasks[currentTaskId].messages]; const toSubTaskIndex = messages.findLastIndex( - (message: Message) => message.step === 'to_sub_tasks' + (message: Message) => message.step === AgentStep.TO_SUB_TASKS ); // For multi-turn scenarios, always create a new to_sub_tasks message // even if one already exists from a previous task @@ -1044,7 +1057,7 @@ const chatStore = (initial?: Partial) => const { tasks, handleConfirmTask, setIsTaskEdit } = currentStore; const message = tasks[currentId].messages.findLast( - (item) => item.step === 'to_sub_tasks' + (item) => item.step === AgentStep.TO_SUB_TASKS ); const isConfirm = message?.isConfirm || false; const isTakeControl = tasks[currentId].isTakeControl; @@ -1076,7 +1089,7 @@ const chatStore = (initial?: Partial) => id: generateUniqueId(), role: 'agent', content: '', - step: 'notice_card', + step: AgentStep.NOTICE_CARD, }; addMessages(currentTaskId, newNoticeMessage); const shouldAutoConfirm = !!type && !isMultiTurnAfterCompletion; @@ -1102,7 +1115,7 @@ const chatStore = (initial?: Partial) => } agentMessages.data.sub_tasks = agentMessages.data.sub_tasks?.map( (item) => { - item.status = ''; + item.status = TaskStatus.EMPTY; return item; } ); @@ -1132,7 +1145,7 @@ const chatStore = (initial?: Partial) => return; } // Create agent - if (agentMessages.step === 'create_agent') { + if (agentMessages.step === AgentStep.CREATE_AGENT) { const { agent_name, agent_id } = agentMessages.data; if (!agent_name || !agent_id) return; @@ -1188,7 +1201,7 @@ const chatStore = (initial?: Partial) => } return; } - if (agentMessages.step === 'wait_confirm') { + if (agentMessages.step === AgentStep.WAIT_CONFIRM) { const { content, question } = agentMessages.data; setHasWaitComfirm(currentTaskId, true); setIsPending(currentTaskId, false); @@ -1213,7 +1226,7 @@ const chatStore = (initial?: Partial) => id: generateUniqueId(), role: 'user', content: question as string, - step: 'wait_confirm', + step: AgentStep.WAIT_CONFIRM, isConfirm: false, }); } @@ -1221,13 +1234,13 @@ const chatStore = (initial?: Partial) => id: generateUniqueId(), role: 'agent', content: content as string, - step: 'wait_confirm', + step: AgentStep.WAIT_CONFIRM, isConfirm: false, }); return; } // Task State - if (agentMessages.step === 'task_state') { + if (agentMessages.step === AgentStep.TASK_STATE) { const { state, task_id, result, failure_count } = agentMessages.data; if (!state && !task_id) return; @@ -1247,7 +1260,7 @@ const chatStore = (initial?: Partial) => targetTaskAssigningIndex ].tasks.findIndex((task: TaskInfo) => task.id === task_id); taskAssigning[targetTaskAssigningIndex].tasks[taskIndex].status = - state === 'DONE' ? 'completed' : 'failed'; + state === 'DONE' ? TaskStatus.COMPLETED : TaskStatus.FAILED; taskAssigning[targetTaskAssigningIndex].tasks[ taskIndex ].failure_count = failure_count || 0; @@ -1288,7 +1301,7 @@ const chatStore = (initial?: Partial) => id: generateUniqueId(), role: 'agent', content: targetResult, - step: 'failed', + step: AgentStep.FAILED, }); } } @@ -1296,7 +1309,7 @@ const chatStore = (initial?: Partial) => if (targetTaskIndex !== -1) { console.log('targetTaskIndex', targetTaskIndex, state); taskRunning[targetTaskIndex].status = - state === 'DONE' ? 'completed' : 'failed'; + state === 'DONE' ? TaskStatus.COMPLETED : TaskStatus.FAILED; } setTaskRunning(currentTaskId, taskRunning); setTaskAssigning(currentTaskId, taskAssigning); @@ -1306,7 +1319,7 @@ const chatStore = (initial?: Partial) => * @deprecated * Side effect handled on top of the message handler */ - if (agentMessages.step === 'new_task_state') { + if (agentMessages.step === AgentStep.NEW_TASK_STATE) { const { task_id, content, @@ -1323,8 +1336,8 @@ const chatStore = (initial?: Partial) => // Activate agent if ( - agentMessages.step === 'activate_agent' || - agentMessages.step === 'deactivate_agent' + agentMessages.step === AgentStep.ACTIVATE_AGENT || + agentMessages.step === AgentStep.DEACTIVATE_AGENT ) { let taskAssigning = [...tasks[currentTaskId].taskAssigning]; let taskRunning = [...tasks[currentTaskId].taskRunning]; @@ -1346,32 +1359,33 @@ const chatStore = (initial?: Partial) => // } const message = filterMessage(agentMessages); - if (agentMessages.step === 'activate_agent') { - taskAssigning[agentIndex].status = 'running'; + if (agentMessages.step === AgentStep.ACTIVATE_AGENT) { + taskAssigning[agentIndex].status = AgentStatusValue.RUNNING; if (message) { taskAssigning[agentIndex].log.push({ ...agentMessages, - status: 'running', + status: AgentMessageStatus.RUNNING, }); } const taskIndex = taskRunning.findIndex( (task) => task.id === process_task_id ); if (taskIndex !== -1 && taskRunning![taskIndex].status) { - taskRunning![taskIndex].agent!.status = 'running'; - taskRunning![taskIndex]!.status = 'running'; + taskRunning![taskIndex].agent!.status = + AgentStatusValue.RUNNING; + taskRunning![taskIndex]!.status = TaskStatus.RUNNING; const task = taskAssigning[agentIndex].tasks.find( (task: TaskInfo) => task.id === process_task_id ); if (task) { - task.status = 'running'; + task.status = TaskStatus.RUNNING; } } setTaskRunning(currentTaskId, [...taskRunning]); setTaskAssigning(currentTaskId, [...taskAssigning]); } - if (agentMessages.step === 'deactivate_agent') { + if (agentMessages.step === AgentStep.DEACTIVATE_AGENT) { if (message) { const index = taskAssigning[agentIndex].log.findLastIndex( (log) => @@ -1379,7 +1393,8 @@ const chatStore = (initial?: Partial) => log.data.toolkit_name === agentMessages.data.toolkit_name ); if (index != -1) { - taskAssigning[agentIndex].log[index].status = 'completed'; + taskAssigning[agentIndex].log[index].status = + AgentMessageStatus.COMPLETED; setTaskAssigning(currentTaskId, [...taskAssigning]); } } @@ -1405,7 +1420,7 @@ const chatStore = (initial?: Partial) => return; } // Assign task - if (agentMessages.step === 'assign_task') { + if (agentMessages.step === AgentStep.ASSIGN_TASK) { if ( !agentMessages.data?.assignee_id || !agentMessages.data?.task_id @@ -1475,21 +1490,25 @@ const chatStore = (initial?: Partial) => // Clear logs from the assignee agent that are related to this task // This prevents logs from previous attempts appearing in the reassigned task // This needs to happen whether it's a reassignment to a different agent or a retry with the same agent - if (taskState !== 'waiting' && failure_count && failure_count > 0) { + if ( + taskState !== TaskStatus.WAITING && + failure_count && + failure_count > 0 + ) { taskAssigning[assigneeAgentIndex].log = taskAssigning[ assigneeAgentIndex ].log.filter((log) => log.data.process_task_id !== task_id); } // Handle task assignment to taskAssigning based on state - if (taskState === 'waiting') { + if (taskState === TaskStatus.WAITING) { if ( !taskAssigning[assigneeAgentIndex].tasks.find( (item) => item.id === task_id ) ) { taskAssigning[assigneeAgentIndex].tasks.push( - task ?? { id: task_id, content, status: 'waiting' } + task ?? { id: task_id, content, status: TaskStatus.WAITING } ); } setTaskAssigning(currentTaskId, [...taskAssigning]); @@ -1505,7 +1524,7 @@ const chatStore = (initial?: Partial) => // Task already exists, update its status taskAssigning[assigneeAgentIndex].tasks[ existingTaskIndex - ].status = 'running'; + ].status = TaskStatus.RUNNING; if (failure_count !== 0) { taskAssigning[assigneeAgentIndex].tasks[ existingTaskIndex @@ -1517,12 +1536,16 @@ const chatStore = (initial?: Partial) => if (task) { taskTemp = JSON.parse(JSON.stringify(task)); taskTemp.failure_count = 0; - taskTemp.status = 'running'; + taskTemp.status = TaskStatus.RUNNING; taskTemp.toolkits = []; taskTemp.report = ''; } taskAssigning[assigneeAgentIndex].tasks.push( - taskTemp ?? { id: task_id, content, status: 'running' } + taskTemp ?? { + id: task_id, + content, + status: TaskStatus.RUNNING, + } ); } } @@ -1531,13 +1554,19 @@ const chatStore = (initial?: Partial) => if (taskRunningIndex === -1) { // Task not in taskRunning, add it if (task) { - task.status = taskState === 'waiting' ? 'waiting' : 'running'; + task.status = + taskState === TaskStatus.WAITING + ? TaskStatus.WAITING + : TaskStatus.RUNNING; } taskRunning!.push( task ?? { id: task_id, content, - status: taskState === 'waiting' ? 'waiting' : 'running', + status: + taskState === TaskStatus.WAITING + ? TaskStatus.WAITING + : TaskStatus.RUNNING, agent: JSON.parse(JSON.stringify(taskAgent)), } ); @@ -1545,7 +1574,10 @@ const chatStore = (initial?: Partial) => // Task already in taskRunning, update it taskRunning![taskRunningIndex] = { ...taskRunning![taskRunningIndex], - status: taskState === 'waiting' ? 'waiting' : 'running', + status: + taskState === TaskStatus.WAITING + ? TaskStatus.WAITING + : TaskStatus.RUNNING, agent: JSON.parse(JSON.stringify(taskAgent)), }; } @@ -1555,7 +1587,7 @@ const chatStore = (initial?: Partial) => return; } // Activate Toolkit - if (agentMessages.step === 'activate_toolkit') { + if (agentMessages.step === AgentStep.ACTIVATE_TOOLKIT) { // add log let taskAssigning = [...tasks[currentTaskId].taskAssigning]; const resolvedProcessTaskId = resolveProcessTaskIdForToolkitEvent( @@ -1654,7 +1686,7 @@ const chatStore = (initial?: Partial) => toolkitName: toolkit_name, toolkitMethods: method_name, message: normalizeToolkitMessage(message.data.message), - toolkitStatus: 'running' as AgentStatus, + toolkitStatus: AgentStatusValue.RUNNING, }; // Update taskAssigning if we found the agent @@ -1665,13 +1697,13 @@ const chatStore = (initial?: Partial) => if (task) { task.toolkits ??= []; task.toolkits.push({ ...toolkit }); - task.status = 'running'; + task.status = TaskStatus.RUNNING; setTaskAssigning(currentTaskId, [...taskAssigning]); } } // Always update taskRunning (even if assigneeAgentIndex is -1) - taskRunning![taskIndex].status = 'running'; + taskRunning![taskIndex].status = TaskStatus.RUNNING; taskRunning![taskIndex].toolkits ??= []; taskRunning![taskIndex].toolkits.push({ ...toolkit }); } @@ -1681,7 +1713,7 @@ const chatStore = (initial?: Partial) => return; } // Deactivate Toolkit - if (agentMessages.step === 'deactivate_toolkit') { + if (agentMessages.step === AgentStep.DEACTIVATE_TOOLKIT) { // add log let taskAssigning = [...tasks[currentTaskId].taskAssigning]; const resolvedProcessTaskId = resolveProcessTaskIdForToolkitEvent( @@ -1709,14 +1741,15 @@ const chatStore = (initial?: Partial) => toolkit.toolkitName === agentMessages.data.toolkit_name && toolkit.toolkitMethods === agentMessages.data.method_name && - toolkit.toolkitStatus === 'running' + toolkit.toolkitStatus === AgentStatusValue.RUNNING ); }); if (task.toolkits && index !== -1 && index !== undefined) { task.toolkits[index].message = `${normalizeToolkitMessage(task.toolkits[index].message)}\n${normalizeToolkitMessage(message.data.message)}`.trim(); - task.toolkits[index].toolkitStatus = 'completed'; + task.toolkits[index].toolkitStatus = + AgentStatusValue.COMPLETED; } // task.toolkits?.unshift({ // toolkitName: agentMessages.data.toolkit_name as string, @@ -1756,7 +1789,7 @@ const chatStore = (initial?: Partial) => message: normalizeToolkitMessage( targetMessage.data.message ), - toolkitStatus: 'completed', + toolkitStatus: AgentStatusValue.COMPLETED, }); } } @@ -1766,7 +1799,7 @@ const chatStore = (initial?: Partial) => return; } // Terminal - if (agentMessages.step === 'terminal') { + if (agentMessages.step === AgentStep.TERMINAL) { addTerminal( currentTaskId, agentMessages.data.process_task_id as string, @@ -1775,7 +1808,7 @@ const chatStore = (initial?: Partial) => return; } // Write File - if (agentMessages.step === 'write_file') { + if (agentMessages.step === AgentStep.WRITE_FILE) { console.log('write_to_file', agentMessages.data); setNuwFileNum(currentTaskId, tasks[currentTaskId].nuwFileNum + 1); const { file_path } = agentMessages.data; @@ -1796,15 +1829,15 @@ const chatStore = (initial?: Partial) => return; } - if (agentMessages.step === 'budget_not_enough') { + if (agentMessages.step === AgentStep.BUDGET_NOT_ENOUGH) { console.log('error', agentMessages.data); showCreditsToast(); - setStatus(currentTaskId, 'pause'); + setStatus(currentTaskId, ChatTaskStatus.PAUSE); uploadLog(currentTaskId, type); return; } - if (agentMessages.step === 'context_too_long') { + if (agentMessages.step === AgentStep.CONTEXT_TOO_LONG) { console.error('Context too long:', agentMessages.data); const currentLength = agentMessages.data.current_length || 0; const maxLength = agentMessages.data.max_length || 100000; @@ -1821,12 +1854,12 @@ const chatStore = (initial?: Partial) => // Set flag to block input and set status to pause setIsContextExceeded(currentTaskId, true); - setStatus(currentTaskId, 'pause'); + setStatus(currentTaskId, ChatTaskStatus.PAUSE); uploadLog(currentTaskId, type); return; } - if (agentMessages.step === 'error') { + if (agentMessages.step === AgentStep.ERROR) { try { console.error('Model error:', agentMessages.data); @@ -1852,8 +1885,11 @@ const chatStore = (initial?: Partial) => // Update taskRunning - mark non-completed tasks as failed taskRunning = taskRunning.map((task) => { - if (task.status !== 'completed' && task.status !== 'failed') { - task.status = 'failed'; + if ( + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.FAILED + ) { + task.status = TaskStatus.FAILED; } return task; }); @@ -1861,8 +1897,11 @@ const chatStore = (initial?: Partial) => // Update taskAssigning - mark non-completed tasks as failed taskAssigning = taskAssigning.map((agent) => { agent.tasks = agent.tasks.map((task) => { - if (task.status !== 'completed' && task.status !== 'failed') { - task.status = 'failed'; + if ( + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.FAILED + ) { + task.status = TaskStatus.FAILED; } return task; }); @@ -1874,7 +1913,7 @@ const chatStore = (initial?: Partial) => setTaskAssigning(currentTaskId, taskAssigning); // Complete the current task with error status - setStatus(currentTaskId, 'finished'); + setStatus(currentTaskId, ChatTaskStatus.FINISHED); setIsPending(currentTaskId, false); // Add error message to the current task @@ -1927,7 +1966,7 @@ const chatStore = (initial?: Partial) => } // Handle add_task events for project store - if (agentMessages.step === 'add_task') { + if (agentMessages.step === AgentStep.ADD_TASK) { try { const taskData = agentMessages.data; if (taskData && taskData.project_id && taskData.content) { @@ -1964,7 +2003,7 @@ const chatStore = (initial?: Partial) => } // Handle remove_task events for project store - if (agentMessages.step === 'remove_task') { + if (agentMessages.step === AgentStep.REMOVE_TASK) { try { const taskIdToRemove = agentMessages.data.task_id as string; if (taskIdToRemove) { @@ -1999,7 +2038,7 @@ const chatStore = (initial?: Partial) => return; } - if (agentMessages.step === 'end') { + if (agentMessages.step === AgentStep.END) { // compute task time console.log( 'tasks[taskId].snapshotsTemp', @@ -2107,11 +2146,11 @@ const chatStore = (initial?: Partial) => taskAssigning = taskAssigning.map((agent) => { agent.tasks = agent.tasks.map((task) => { if ( - task.status !== 'completed' && - task.status !== 'failed' && + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.FAILED && !type ) { - task.status = 'skipped'; + task.status = TaskStatus.SKIPPED; } return task; }); @@ -2121,11 +2160,11 @@ const chatStore = (initial?: Partial) => taskRunning = taskRunning.map((task) => { console.log('task.status', task.status); if ( - task.status !== 'completed' && - task.status !== 'failed' && + task.status !== TaskStatus.COMPLETED && + task.status !== TaskStatus.FAILED && !type ) { - task.status = 'skipped'; + task.status = TaskStatus.SKIPPED; } return task; }); @@ -2158,7 +2197,7 @@ const chatStore = (initial?: Partial) => let summary = endMessage.match(/(.*?)<\/summary>/)?.[1]; let newMessage: Message | null = null; const agent_summary_end = tasks[currentTaskId].messages.findLast( - (message: Message) => message.step === 'agent_summary_end' + (message: Message) => message.step === AgentStep.AGENT_SUMMARY_END ); console.log('summary', summary); if (summary) { @@ -2181,7 +2220,7 @@ const chatStore = (initial?: Partial) => addMessages(currentTaskId, newMessage); setIsPending(currentTaskId, false); - setStatus(currentTaskId, 'finished'); + setStatus(currentTaskId, ChatTaskStatus.FINISHED); // completed tasks move to history setUpdateCount(); @@ -2189,7 +2228,7 @@ const chatStore = (initial?: Partial) => return; } - if (agentMessages.step === 'notice') { + if (agentMessages.step === AgentStep.NOTICE) { if (agentMessages.data.process_task_id !== '') { let taskAssigning = [...tasks[currentTaskId].taskAssigning]; @@ -2209,7 +2248,7 @@ const chatStore = (initial?: Partial) => toolkitName: 'notice', toolkitMethods: '', message: agentMessages.data.notice as string, - toolkitStatus: 'running' as AgentStatus, + toolkitStatus: AgentStatusValue.RUNNING, }; if (assigneeAgentIndex !== -1 && task) { task.toolkits ??= []; @@ -2219,14 +2258,14 @@ const chatStore = (initial?: Partial) => } else { const messages = [...tasks[currentTaskId].messages]; const noticeCardIndex = messages.findLastIndex( - (message) => message.step === 'notice_card' + (message) => message.step === AgentStep.NOTICE_CARD ); if (noticeCardIndex === -1) { const newMessage: Message = { id: generateUniqueId(), role: 'agent', content: '', - step: 'notice_card', + step: AgentStep.NOTICE_CARD, }; addMessages(currentTaskId, newMessage); } @@ -2237,8 +2276,8 @@ const chatStore = (initial?: Partial) => } return; } - if (['sync'].includes(agentMessages.step)) return; - if (agentMessages.step === 'ask') { + if (agentMessages.step === AgentStep.SYNC) return; + if (agentMessages.step === AgentStep.ASK) { if (tasks[currentTaskId].activeAsk != '') { const newMessage: Message = { id: generateUniqueId(), @@ -2568,10 +2607,7 @@ const chatStore = (initial?: Partial) => }, })); }, - setStatus( - taskId: string, - status: 'running' | 'finished' | 'pending' | 'pause' - ) { + setStatus(taskId: string, status: ChatTaskStatusType) { set((state) => ({ ...state, tasks: { @@ -2630,7 +2666,7 @@ const chatStore = (initial?: Partial) => // where backend sends to_sub_tasks SSE event before we mark task as confirmed let messages = [...tasks[taskId].messages]; const cardTaskIndex = messages.findLastIndex( - (message) => message.step === 'to_sub_tasks' + (message) => message.step === AgentStep.TO_SUB_TASKS ); if (cardTaskIndex !== -1) { messages[cardTaskIndex] = { @@ -2648,7 +2684,7 @@ const chatStore = (initial?: Partial) => await fetchPost(`/task/${project_id}/start`, {}); setActiveWorkSpace(taskId, 'workflow'); - setStatus(taskId, 'running'); + setStatus(taskId, ChatTaskStatus.RUNNING); } // Reset editing state after manual confirmation so next round can auto-start diff --git a/src/store/projectStore.ts b/src/store/projectStore.ts index cb7db330..5803dc17 100644 --- a/src/store/projectStore.ts +++ b/src/store/projectStore.ts @@ -15,6 +15,7 @@ import { generateUniqueId } from '@/lib'; import { create } from 'zustand'; import { createChatStoreInstance, VanillaChatStore } from './chatStore'; +import { ChatTaskStatus } from '@/types/constants'; export enum ProjectType { NORMAL = 'normal', @@ -173,7 +174,7 @@ const isEmptyProject = (project: Project): boolean => { task.summaryTask === '' && task.progressValue === 0 && task.isPending === false && - task.status === 'pending' && + task.status === ChatTaskStatus.PENDING && task.taskTime === 0 && task.tokens === 0 && task.elapsed === 0 && diff --git a/src/types/chatbox.d.ts b/src/types/chatbox.d.ts index d429599e..f7b46851 100644 --- a/src/types/chatbox.d.ts +++ b/src/types/chatbox.d.ts @@ -12,6 +12,8 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import type { AgentStepType, AgentMessageStatusType, TaskStatusType, ChatTaskStatusType, AgentStatusType } from './constants'; + // Global type definitions for ChatBox component declare global { @@ -40,7 +42,7 @@ declare global { report?: string | undefined; id: string; content: string; - status?: string; + status?: TaskStatusType; agent?: Agent; terminal?: string[]; fileList?: FileInfo[]; @@ -60,7 +62,7 @@ declare global { filePath: string; } - type AgentStatus = 'pending' | 'running' | 'completed' | 'failed'; + type AgentStatus = AgentStatusType; interface ActiveWebView { id: string; @@ -92,7 +94,7 @@ declare global { id: string; role: 'user' | 'agent'; content: string; - step?: string; + step?: AgentStepType; agent_id?: string; isConfirm?: boolean; taskType?: 1 | 2 | 3; @@ -110,7 +112,7 @@ declare global { } interface AgentMessage { - step: string; + step: AgentStepType; data: { project_id?: string; failure_count?: number; @@ -140,7 +142,7 @@ declare global { max_length?: number; text?: string; }; - status?: 'running' | 'filled' | 'completed'; + status?: AgentMessageStatusType; } type AgentNameType = diff --git a/src/types/constants.ts b/src/types/constants.ts new file mode 100644 index 00000000..913bd343 --- /dev/null +++ b/src/types/constants.ts @@ -0,0 +1,97 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +/** + * SSE step values received from the backend in AgentMessage.step. + */ +export const AgentStep = { + CONFIRMED: 'confirmed', + NEW_TASK_STATE: 'new_task_state', + END: 'end', + WAIT_CONFIRM: 'wait_confirm', + DECOMPOSE_TEXT: 'decompose_text', + TO_SUB_TASKS: 'to_sub_tasks', + CREATE_AGENT: 'create_agent', + TASK_STATE: 'task_state', + ACTIVATE_AGENT: 'activate_agent', + DEACTIVATE_AGENT: 'deactivate_agent', + ASSIGN_TASK: 'assign_task', + ACTIVATE_TOOLKIT: 'activate_toolkit', + DEACTIVATE_TOOLKIT: 'deactivate_toolkit', + TERMINAL: 'terminal', + WRITE_FILE: 'write_file', + BUDGET_NOT_ENOUGH: 'budget_not_enough', + CONTEXT_TOO_LONG: 'context_too_long', + ERROR: 'error', + ADD_TASK: 'add_task', + REMOVE_TASK: 'remove_task', + NOTICE: 'notice', + ASK: 'ask', + SYNC: 'sync', + NOTICE_CARD: 'notice_card', + FAILED: 'failed', + AGENT_SUMMARY_END: 'agent_summary_end', +} as const; + +export type AgentStepType = (typeof AgentStep)[keyof typeof AgentStep]; + +/** + * Status values on AgentMessage.status (SSE message lifecycle). + */ +export const AgentMessageStatus = { + RUNNING: 'running', + FILLED: 'filled', + COMPLETED: 'completed', +} as const; + +export type AgentMessageStatusType = (typeof AgentMessageStatus)[keyof typeof AgentMessageStatus]; + +/** + * Status values for TaskInfo (individual sub-task progress). + */ +export const TaskStatus = { + COMPLETED: 'completed', + FAILED: 'failed', + SKIPPED: 'skipped', + WAITING: 'waiting', + RUNNING: 'running', + BLOCKED: 'blocked', + EMPTY: '', +} as const; + +export type TaskStatusType = (typeof TaskStatus)[keyof typeof TaskStatus]; + +/** + * Top-level task status in the ChatStore Task interface. + */ +export const ChatTaskStatus = { + RUNNING: 'running', + FINISHED: 'finished', + PENDING: 'pending', + PAUSE: 'pause', +} as const; + +export type ChatTaskStatusType = (typeof ChatTaskStatus)[keyof typeof ChatTaskStatus]; + +/** + * Status values for individual agent lifecycle (toolkit operations, agent progress). + */ +export const AgentStatusValue = { + PENDING: 'pending', + RUNNING: 'running', + COMPLETED: 'completed', + FAILED: 'failed', +} as const; + +export type AgentStatusType = (typeof AgentStatusValue)[keyof typeof AgentStatusValue]; From 649bdc6822df7ed9d0caf8d50b55aea994173e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E9=B9=8F=E9=93=96?= <104722516+LuoPengcheng12138@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:56:51 +0800 Subject: [PATCH 2/2] add callback methods for task updates/decomposition (#1127) Co-authored-by: bytecii <994513625@qq.com> --- .../app/utils/telemetry/workforce_metrics.py | 65 +++++++++++++++++++ .../utils/telemetry/test_workforce_metrics.py | 57 ++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/backend/app/utils/telemetry/workforce_metrics.py b/backend/app/utils/telemetry/workforce_metrics.py index c20fe385..df7d2aae 100644 --- a/backend/app/utils/telemetry/workforce_metrics.py +++ b/backend/app/utils/telemetry/workforce_metrics.py @@ -23,8 +23,10 @@ import camel from camel.societies.workforce.events import (LogEvent, TaskAssignedEvent, TaskCompletedEvent, TaskCreatedEvent, + TaskDecomposedEvent, TaskFailedEvent, TaskStartedEvent, + TaskUpdatedEvent, WorkerCreatedEvent) from camel.societies.workforce.workforce_metrics import WorkforceMetrics from opentelemetry import trace @@ -58,11 +60,16 @@ ATTR_TASK_DESCRIPTION = "eigent.task.description" ATTR_TASK_PARENT_ID = "eigent.task.parent_id" ATTR_TASK_TYPE = "eigent.task.type" ATTR_TASK_STATUS = "eigent.task.status" +ATTR_TASK_UPDATE_TYPE = "eigent.task.update_type" +ATTR_TASK_UPDATE_OLD_VALUE = "eigent.task.update.old_value" +ATTR_TASK_UPDATE_NEW_VALUE = "eigent.task.update.new_value" +ATTR_TASK_UPDATE_METADATA = "eigent.task.update.metadata" ATTR_TASK_QUEUE_TIME_SECONDS = "eigent.task.queue_time_seconds" ATTR_TASK_PROCESSING_TIME_SECONDS = "eigent.task.processing_time_seconds" ATTR_TASK_QUALITY_SCORE = "eigent.task.quality_score" ATTR_TASK_TIMESTAMP = "eigent.task.timestamp" ATTR_TASK_DEPENDENCIES = "eigent.task.dependencies" +ATTR_TASK_SUBTASK_IDS = "eigent.task.subtask_ids" # Attribute keys for eigent.worker namespace ATTR_WORKER_ID = "eigent.worker.id" @@ -86,7 +93,9 @@ TRACER_NAME_WORKFORCE = "eigent.workforce" SPAN_WORKFORCE_EXECUTION = "workforce.execution" SPAN_WORKER_CREATED = "worker.created" SPAN_TASK_CREATED = "task.created" +SPAN_TASK_DECOMPOSED = "task.decomposed" SPAN_TASK_ASSIGNED = "task.assigned" +SPAN_TASK_UPDATED = "task.updated" SPAN_TASK_EXECUTION = "task.execution" SPAN_LOG_MESSAGE = "log.message" SPAN_ALL_TASKS_COMPLETED = "workforce.all_tasks_completed" @@ -318,6 +327,27 @@ class WorkforceMetricsCallback(WorkforceMetrics): span.set_status(Status(StatusCode.OK)) + def log_task_decomposed(self, event: TaskDecomposedEvent) -> None: + """Log task decomposition as a span. + + Args: + event (TaskDecomposedEvent): Task decomposed event from CAMEL + """ + if not self.enabled: + return + + ctx = trace.set_span_in_context(self.root_span) + with self.tracer.start_as_current_span(SPAN_TASK_DECOMPOSED, + context=ctx) as span: + span.set_attribute(ATTR_TASK_PARENT_ID, event.parent_task_id) + span.set_attribute(ATTR_PROJECT_ID, self.project_id) + + if event.subtask_ids: + span.set_attribute(ATTR_TASK_SUBTASK_IDS, + json.dumps(event.subtask_ids)) + + span.set_status(Status(StatusCode.OK)) + def log_task_assigned(self, event: TaskAssignedEvent) -> None: """Log task assignment as a span. @@ -345,6 +375,41 @@ class WorkforceMetricsCallback(WorkforceMetrics): span.set_status(Status(StatusCode.OK)) + def log_task_updated(self, event: TaskUpdatedEvent) -> None: + """Log task update events (replan/reassign/manual) as a span. + + Args: + event: Task updated event from CAMEL + """ + if not self.enabled: + return + + ctx = trace.set_span_in_context(self.root_span) + with self.tracer.start_as_current_span(SPAN_TASK_UPDATED, + context=ctx) as span: + span.set_attribute(ATTR_TASK_ID, event.task_id) + span.set_attribute(ATTR_PROJECT_ID, self.project_id) + span.set_attribute(ATTR_TASK_UPDATE_TYPE, event.update_type) + + if event.worker_id: + span.set_attribute(ATTR_WORKER_ID, event.worker_id) + if event.parent_task_id: + span.set_attribute(ATTR_TASK_PARENT_ID, event.parent_task_id) + if event.old_value is not None: + span.set_attribute(ATTR_TASK_UPDATE_OLD_VALUE, + event.old_value) + if event.new_value is not None: + span.set_attribute(ATTR_TASK_UPDATE_NEW_VALUE, + event.new_value) + if event.metadata: + span.set_attribute(ATTR_TASK_UPDATE_METADATA, + json.dumps(event.metadata)) + if hasattr(event, 'timestamp') and event.timestamp: + span.set_attribute(ATTR_TASK_TIMESTAMP, + event.timestamp.isoformat()) + + span.set_status(Status(StatusCode.OK)) + def log_task_started(self, event: TaskStartedEvent) -> None: """Log task start and create a span for the task execution. diff --git a/backend/tests/unit/utils/telemetry/test_workforce_metrics.py b/backend/tests/unit/utils/telemetry/test_workforce_metrics.py index ab529d95..d30415b5 100644 --- a/backend/tests/unit/utils/telemetry/test_workforce_metrics.py +++ b/backend/tests/unit/utils/telemetry/test_workforce_metrics.py @@ -1,3 +1,17 @@ +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + """Tests for workforce metrics telemetry.""" from datetime import datetime from unittest.mock import MagicMock, Mock, patch @@ -8,8 +22,10 @@ from app.utils.telemetry.workforce_metrics import WorkforceMetricsCallback from camel.societies.workforce.events import (LogEvent, TaskAssignedEvent, TaskCompletedEvent, TaskCreatedEvent, + TaskDecomposedEvent, TaskFailedEvent, TaskStartedEvent, + TaskUpdatedEvent, WorkerCreatedEvent) @@ -90,6 +106,24 @@ def test_log_task_created(metrics_callback): assert mock_span.set_status.called +def test_log_task_decomposed(metrics_callback): + """Test log_task_decomposed function.""" + event = TaskDecomposedEvent( + parent_task_id="parent_1", + subtask_ids=["task_1", "task_2"], + ) + + mock_span = Mock() + metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock())) + + metrics_callback.log_task_decomposed(event) + + # Verify span attributes were set + assert mock_span.set_attribute.called + assert mock_span.set_status.called + + def test_log_task_assigned(metrics_callback): """Test log_task_assigned function.""" event = TaskAssignedEvent( @@ -110,6 +144,29 @@ def test_log_task_assigned(metrics_callback): assert mock_span.set_status.called +def test_log_task_updated(metrics_callback): + """Test log_task_updated function.""" + event = TaskUpdatedEvent( + task_id="task_1", + worker_id="worker_1", + update_type="replan", + old_value="old plan", + new_value="new plan", + parent_task_id="parent_1", + metadata={"source": "recovery"}, + ) + + mock_span = Mock() + metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock())) + + metrics_callback.log_task_updated(event) + + # Verify span attributes were set + assert mock_span.set_attribute.called + assert mock_span.set_status.called + + def test_log_task_started(metrics_callback): """Test log_task_started function.""" event = TaskStartedEvent(task_id="task_1", worker_id="worker_1")