diff --git a/frontend/src/lib/components/FlowStatusViewerInner.svelte b/frontend/src/lib/components/FlowStatusViewerInner.svelte index da602b0980..4a5ce70432 100644 --- a/frontend/src/lib/components/FlowStatusViewerInner.svelte +++ b/frontend/src/lib/components/FlowStatusViewerInner.svelte @@ -49,6 +49,7 @@ import JobAssetsViewer from './assets/JobAssetsViewer.svelte' import McpToolCallDetails from './McpToolCallDetails.svelte' import { SelectionManager } from './graph/selectionUtils.svelte' + import { useThrottle } from 'runed' let { flowState: flowStateStore, @@ -503,211 +504,187 @@ } let jobMissingStartedAt: Record = {} - let lastSelectedLoopSwitch: number | undefined - let selectedLoopSwitchTimeout: number | undefined = undefined + + let setSelectedLoopSwitch = useThrottle((lastStarted: string, mod: FlowStatusModule) => { + let position = mod.flow_jobs?.indexOf(lastStarted) + if (!position) return + // we wait a bit for suspended steps, to make sure the current iteration is updated + // when resumed before going forward. This avoid the duplicate resume forms bug. + setIteration(position, lastStarted, false, mod.id ?? '', true) + }, 2000) function updateInnerModules() { - if (localModuleStates) { - innerModules?.forEach((mod, i) => { - if (mod.type === 'WaitingForEvents' && innerModules?.[i - 1]?.type === 'Success') { - setModuleState(mod.id ?? '', { - type: mod.type, - args: job?.args, - tag: job?.tag, - script_hash: job?.script_hash + if (!localModuleStates || !innerModules) return + for (let i = 0; i < innerModules.length; i++) { + let mod = innerModules[i] + let prev = innerModules[i - 1] + if (mod.type === 'WaitingForEvents' && prev?.type === 'Success') { + setModuleState(mod.id ?? '', { + type: mod.type, + args: job?.args, + tag: job?.tag, + script_hash: job?.script_hash + }) + } else if ( + mod.type === 'WaitingForExecutor' && + localModuleStates[mod.id ?? '']?.scheduled_for == undefined + ) { + JobService.getJob({ + workspace: workspaceId ?? $workspaceStore ?? '', + id: mod.job ?? '', + noLogs: true, + noCode: true + }) + .then((job) => { + const newState = { + type: mod.type, + scheduled_for: job?.['scheduled_for'], + job_id: job?.id, + parent_module: mod['parent_module'], + args: job?.args, + tag: job?.tag, + script_hash: job?.script_hash + } + setModuleState(mod.id ?? '', newState) }) - } else if ( - mod.type === 'WaitingForExecutor' && - localModuleStates[mod.id ?? '']?.scheduled_for == undefined - ) { - JobService.getJob({ - workspace: workspaceId ?? $workspaceStore ?? '', - id: mod.job ?? '', - noLogs: true, - noCode: true + .catch((e) => { + console.error(`Could not load inner module for job ${mod.job}`, e) }) - .then((job) => { - const newState = { - type: mod.type, - scheduled_for: job?.['scheduled_for'], - job_id: job?.id, - parent_module: mod['parent_module'], - args: job?.args, - tag: job?.tag, - script_hash: job?.script_hash - } + } else if ( + (mod.flow_jobs || mod.branch_chosen) && + (mod.type == 'Success' || mod.type == 'Failure') && + !['Success', 'Failure'].includes(localModuleStates?.[mod.id ?? '']?.type) + ) { + let branchChosen = mod.branch_chosen + ? { + branchChosen: + mod.branch_chosen.type == 'default' ? 0 : (mod.branch_chosen.branch ?? 0) + 1 + } + : {} + setModuleState(mod.id ?? '', { type: mod.type, ...branchChosen }, true) + } else if (isForloopSelected) { + setModuleState(mod.id ?? '', {}, true) + } - setModuleState(mod.id ?? '', newState) + if (mod.flow_jobs_success || mod.flow_jobs_duration || mod.flow_jobs) { + setModuleState(mod.id ?? '', { + flow_jobs_success: mod.flow_jobs_success, + flow_jobs_duration: mod.flow_jobs_duration, + flow_jobs: mod.flow_jobs, + iteration_total: mod.iterator?.itered_len ?? mod.flow_jobs?.length + }) + } + + if (mod.flow_jobs_duration && mod.flow_jobs) { + let key = buildSubflowKey(mod.id ?? '', prefix) + let durationStatuses = Object.fromEntries( + mod.flow_jobs.map((flowJobId, idx) => { + let started_at_str = mod.flow_jobs_duration?.started_at?.[idx] + let started_at = started_at_str ? new Date(started_at_str).getTime() : undefined + let duration_ms = mod.flow_jobs_duration?.duration_ms?.[idx] + if (started_at == undefined) { + let missingStartedAt = jobMissingStartedAt[flowJobId] + if (missingStartedAt != 'P') { + started_at = missingStartedAt + } + } else { + delete jobMissingStartedAt[flowJobId] + } + return [ + flowJobId, + { + created_at: started_at, + started_at: started_at, + duration_ms: duration_ms + } + ] + }) + ) + let missingStartedAtIds = Object.keys(durationStatuses) + .filter( + (id) => durationStatuses[id].created_at == undefined && jobMissingStartedAt[id] != 'P' + ) + .slice(0, 100) + + updateDurationStatuses(key, durationStatuses) + + if (missingStartedAtIds.length > 0) { + missingStartedAtIds.forEach((id) => { + jobMissingStartedAt[id] = 'P' + }) + JobService.getStartedAtByIds({ + workspace: workspaceId ?? $workspaceStore ?? '', + requestBody: missingStartedAtIds + }) + .then((jobs) => { + let lastStarted: string | undefined = undefined + let anySet = false + let nDurationStatuses = localDurationStatuses[key]?.byJob + missingStartedAtIds.forEach((id, idx) => { + const startedAt = jobs[idx] + const time = startedAt ? new Date(startedAt).getTime() : undefined + if (time) { + jobMissingStartedAt[id] = time + } else { + delete jobMissingStartedAt[id] + } + if (nDurationStatuses && time) { + if (!nDurationStatuses[id]?.duration_ms) { + anySet = true + lastStarted = id + nDurationStatuses[id] = { + created_at: time, + started_at: time + } + } + } + }) + if (anySet) { + updateDurationStatuses(key, nDurationStatuses) + lastStarted && setSelectedLoopSwitch(lastStarted, mod) + } }) .catch((e) => { - console.error(`Could not load inner module for job ${mod.job}`, e) + console.error(`Could not load inner module duration status for job ${mod.job}`, e) }) - } else if ( - (mod.flow_jobs || mod.branch_chosen) && - (mod.type == 'Success' || mod.type == 'Failure') && - !['Success', 'Failure'].includes(localModuleStates?.[mod.id ?? '']?.type) - ) { - let branchChosen = mod.branch_chosen - ? { - branchChosen: - mod.branch_chosen.type == 'default' ? 0 : (mod.branch_chosen.branch ?? 0) + 1 - } - : {} - // console.debug('updateInnerModules', mod.id, mod.type, branchChosen) - setModuleState( - mod.id ?? '', - { - type: mod.type, - ...branchChosen - }, - true - ) - } else if (isForloopSelected) { - setModuleState(mod.id ?? '', {}, true) + } else { + setIteration(0, mod.flow_jobs?.[0] ?? '', false, mod.id ?? '', true) } + } - if (mod.flow_jobs_success || mod.flow_jobs_duration || mod.flow_jobs) { - setModuleState(mod.id ?? '', { - flow_jobs_success: mod.flow_jobs_success, - flow_jobs_duration: mod.flow_jobs_duration, - flow_jobs: mod.flow_jobs, - iteration_total: mod.iterator?.itered_len ?? mod.flow_jobs?.length - }) - } - - if (mod.flow_jobs_duration && mod.flow_jobs) { - let key = buildSubflowKey(mod.id ?? '', prefix) - let durationStatuses = Object.fromEntries( - mod.flow_jobs.map((flowJobId, idx) => { - let started_at_str = mod.flow_jobs_duration?.started_at?.[idx] - let started_at = started_at_str ? new Date(started_at_str).getTime() : undefined - let duration_ms = mod.flow_jobs_duration?.duration_ms?.[idx] - if (started_at == undefined) { - let missingStartedAt = jobMissingStartedAt[flowJobId] - if (missingStartedAt != 'P') { - started_at = missingStartedAt - } - } else { - delete jobMissingStartedAt[flowJobId] - } - return [ - flowJobId, - { - created_at: started_at, - started_at: started_at, - duration_ms: duration_ms - } - ] - }) - ) - let missingStartedAtIds = Object.keys(durationStatuses) - .filter( - (id) => durationStatuses[id].created_at == undefined && jobMissingStartedAt[id] != 'P' - ) - .slice(0, 100) - - updateDurationStatuses(key, durationStatuses) - - if (missingStartedAtIds.length > 0) { - missingStartedAtIds.forEach((id) => { - jobMissingStartedAt[id] = 'P' - }) - JobService.getStartedAtByIds({ - workspace: workspaceId ?? $workspaceStore ?? '', - requestBody: missingStartedAtIds - }) - .then((jobs) => { - let lastStarted: string | undefined = undefined - let anySet = false - let nDurationStatuses = localDurationStatuses[key]?.byJob - missingStartedAtIds.forEach((id, idx) => { - const startedAt = jobs[idx] - const time = startedAt ? new Date(startedAt).getTime() : undefined - if (time) { - jobMissingStartedAt[id] = time - } else { - delete jobMissingStartedAt[id] - } - if (nDurationStatuses && time) { - if (!nDurationStatuses[id]?.duration_ms) { - anySet = true - lastStarted = id - nDurationStatuses[id] = { - created_at: time, - started_at: time - } - } - } - }) - if (anySet) { - updateDurationStatuses(key, nDurationStatuses) - selectedLoopSwitchTimeout && clearTimeout(selectedLoopSwitchTimeout) - function setSelectedLoopSwitch() { - if (lastStarted) { - let position = mod.flow_jobs?.indexOf(lastStarted) - if (position != undefined) { - lastSelectedLoopSwitch = new Date().getTime() - console.log('setSelectedLoopSwitch', position, lastStarted) - setIteration(position, lastStarted, false, mod.id ?? '', true) - } - } - } - - if ( - lastSelectedLoopSwitch && - new Date().getTime() - lastSelectedLoopSwitch < 3000 - ) { - selectedLoopSwitchTimeout = setTimeout(() => { - setSelectedLoopSwitch() - }, 2000) - } else { - console.log('setSelectedLoopSwitch') - setSelectedLoopSwitch() - } - } + if (mod.agent_actions && mod.id) { + setModuleState(mod.id, { + agent_actions: mod.agent_actions + }) + mod.agent_actions.forEach((action, idx) => { + if (mod.id) { + if (action.type == 'tool_call') { + const toolCallId = getToolCallId(idx, mod.id, action.module_id) + const success = mod.agent_actions_success?.[idx] + setModuleState(toolCallId, { + job_id: action.job_id, + type: success != undefined ? (success ? 'Success' : 'Failure') : 'InProgress' }) - .catch((e) => { - console.error(`Could not load inner module duration status for job ${mod.job}`, e) + } else if (action.type == 'mcp_tool_call') { + const mcpToolCallId = AI_MCP_TOOL_CALL_PREFIX + '-' + mod.id + '-' + idx + const success = mod.agent_actions_success?.[idx] + setModuleState(mcpToolCallId, { + type: success != undefined ? (success ? 'Success' : 'Failure') : 'InProgress' + }) + } else if (action.type == 'web_search') { + const websearchId = AI_WEBSEARCH_PREFIX + '-' + mod.id + '-' + idx + setModuleState(websearchId, { + type: 'Success' + }) + } else if (action.type == 'message') { + const toolCallId = getToolCallId(idx, mod.id) + setModuleState(toolCallId, { + type: 'Success' }) - } else { - setIteration(0, mod.flow_jobs?.[0] ?? '', false, mod.id ?? '', true) - } - } - - if (mod.agent_actions && mod.id) { - setModuleState(mod.id, { - agent_actions: mod.agent_actions - }) - mod.agent_actions.forEach((action, idx) => { - if (mod.id) { - if (action.type == 'tool_call') { - const toolCallId = getToolCallId(idx, mod.id, action.module_id) - const success = mod.agent_actions_success?.[idx] - setModuleState(toolCallId, { - job_id: action.job_id, - type: success != undefined ? (success ? 'Success' : 'Failure') : 'InProgress' - }) - } else if (action.type == 'mcp_tool_call') { - const mcpToolCallId = AI_MCP_TOOL_CALL_PREFIX + '-' + mod.id + '-' + idx - const success = mod.agent_actions_success?.[idx] - setModuleState(mcpToolCallId, { - type: success != undefined ? (success ? 'Success' : 'Failure') : 'InProgress' - }) - } else if (action.type == 'web_search') { - const websearchId = AI_WEBSEARCH_PREFIX + '-' + mod.id + '-' + idx - setModuleState(websearchId, { - type: 'Success' - }) - } else if (action.type == 'message') { - const toolCallId = getToolCallId(idx, mod.id) - setModuleState(toolCallId, { - type: 'Success' - }) - } } - }) - } - }) + } + }) + } } } @@ -746,7 +723,7 @@ } else { job = newJob } - job?.flow_status && updateStatus(job?.flow_status) + if (job?.flow_status) updateStatus(job?.flow_status) onJobsLoaded?.({ job, force: false }) notAnonynmous = false if (job?.type == 'CompletedJob' && !destroyed) {