humainflow-web/src/app/models/task-execution.ts

253 lines
7.5 KiB
TypeScript

// SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
// SPDX-License-Identifier: AGPL-3.0-or-later
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
import {
FileInputConstraints,
FlowBlockConnection,
FlowData,
FlowNode,
FlowNodeDependency,
FlowPort,
LLMDescriptor
} from './flow';
import { BiasExecutionContext } from './bias-impact';
export type TaskExecutionStatus = 'CREATED' | 'READY' | 'RUNNING' | 'WAITING' | 'SUSPENDED' | 'SUCCESS' | 'ERROR' | 'CANCELLED';
export type TaskExecutionStatusGroup = 'INIT' | 'RUNNING' | 'PAUSED' | 'FINAL';
export type StepStatus =
| 'WAITING_FOR_INPUT'
| 'WAITING_FOR_DEPENDENCY'
| 'WAITING_FOR_INTERACTION'
| 'WAITING_FOR_SUBFLOW'
| 'READY'
| 'RUNNING'
| 'COMPLETED'
| 'SKIPPED'
| 'FAILED'
| 'CANCELLED'
| string;
export type ContainerContinuationPhase =
| 'CHILD_CREATED'
| 'CHILD_RUNNING'
| 'WAITING_FOR_SUBFLOW'
| 'CHILD_COMPLETED'
| string;
export type ExecutionKind = 'TOP_LEVEL' | 'SUBFLOW' | string;
export type SubflowRole = 'MAIN' | 'GUARD' | string;
export type ExecutionEventLogEntry = {
id: string;
timestamp: number;
stepId?: string | null;
nodeId?: string | null;
nodeName?: string | null;
level?: string | null;
type?: string | null;
message?: string | null;
details?: unknown;
};
export type TaskExecution = {
id: string;
name: string;
creationTime: number;
flowId?: string | null;
sourceFlowId?: string | null;
/** The project the source flow belonged to when the run was created. */
projectId?: string | null;
/** Ties together the executions started by one project run; null for a single-flow run. */
projectRunId?: string | null;
runNumber?: number | null;
rerunOfExecutionId?: string | null;
executionKind?: ExecutionKind;
parentExecutionId?: string | null;
parentStepId?: string | null;
parentIterationIndex?: number | null;
subflowRole?: SubflowRole | null;
biasExecutionContext?: BiasExecutionContext;
context: TaskExecutionContext;
interactionSimulationEnabled?: boolean;
simulationAvailable?: boolean;
interactionSimulationDescriptor?: LLMDescriptor;
flowSnapshot?: FlowData;
stepConnections?: FlowBlockConnection[];
stepDependencies?: FlowNodeDependency[];
requiredAuthorizations?: Record<string, TaskExecutionAuthorizationRequirement> | TaskExecutionAuthorizationRequirement[];
providedAuthorizations?: Record<string, unknown>;
missingAuthorizationKeys?: string[];
missingGlobalInputKeys?: string[];
};
export type TaskExecutionGroup = {
id: string;
sourceFlowId: string;
name: string;
firstExecutionId: string;
latestExecutionId: string;
creationTime: number;
lastExecutionTime: number;
executionCount: number;
executions: TaskExecution[];
};
export type TaskExecutionAuthorizationRequirement = {
key: string;
provider: string;
fieldName: string;
description: string;
requiredBySteps: string[];
};
export type TaskExecutionContext = {
inputs: Record<string, unknown>;
globalInputs?: Record<string, unknown>;
globalInputDescriptors?: Record<string, TaskExecutionGlobalInputDescriptor>;
/**
* Values inherited from the flow's project, frozen when the run was created. Read in prompts as
* `${{project.<name>}}`.
*/
projectContext?: Record<string, unknown>;
result: Record<string, unknown>;
partialResult?: Record<string, unknown>;
startTime?: number | null;
endTime?: number | null;
errors: Record<string, string>;
warnings: Record<string, string> | unknown[];
steps: Record<string, TaskExecutionStep>;
status: TaskExecutionStatus;
waitingSteps: string[];
authorizations?: Record<string, unknown>;
executionVariables?: Record<string, unknown>;
executionVariableDescriptors?: Record<string, unknown>;
errorCodes?: Record<string, unknown>;
/**
* One entry per End node the run passed through. This is where a flow's answer lands when the
* last block is wired into an End: the flow `result` is built only from *unconnected* outputs, so
* connecting one into an End moves its value here, as the outcome payload.
*/
outcomes?: TaskExecutionOutcome[];
};
export type TaskExecutionOutcome = {
stepId: string;
code: string;
label: string;
payload: unknown;
timestamp: number;
};
export function normalizeExecutionOutcomes(raw: unknown): TaskExecutionOutcome[] {
if (!Array.isArray(raw)) return [];
return raw
.filter((entry): entry is Record<string, unknown> =>
!!entry && typeof entry === 'object' && !Array.isArray(entry))
.map((entry) => ({
stepId: String(entry['stepId'] ?? ''),
code: String(entry['code'] ?? ''),
label: String(entry['label'] ?? entry['code'] ?? ''),
payload: entry['payload'] ?? null,
timestamp: typeof entry['timestamp'] === 'number' ? entry['timestamp'] : 0
}))
.filter((outcome) => outcome.code.length > 0 || outcome.payload !== null);
}
export type TaskExecutionGlobalInputDescriptor = {
name: string;
kind: string;
value: unknown;
description?: string | null;
cleanupPolicy?: string | null;
multiple?: boolean;
fileConstraints?: FileInputConstraints | null;
};
export type TaskExecutionStep = {
node?: FlowNode;
id: string;
inputs?: TaskExecutionStepInput[];
outputs?: TaskExecutionStepOutput[];
result?: Record<string, unknown>;
status: StepStatus;
started?: boolean;
skipReason?: string | null;
simulated: boolean;
activeInnerExecutionId?: string | null;
containerContinuationPhase?: ContainerContinuationPhase | null;
containerIterationIndex?: number | null;
activeBiasProbes?: Array<{
annotationId: string;
direction: 'BIAS' | 'MITIGATION';
activationMode: string;
instruction?: string;
}>;
};
export type TaskExecutionStepInput = {
descriptor: FlowPort;
value: unknown;
registered: boolean;
set: boolean;
};
export type TaskExecutionStepOutput = {
descriptor: FlowPort;
connected: boolean;
};
export function getExecutionStatusGroup(status: string | null | undefined): TaskExecutionStatusGroup | null {
const normalized = String(status ?? '').toUpperCase();
if (!normalized) return null;
if (normalized === 'CREATED' || normalized === 'READY') return 'INIT';
if (
normalized === 'RUNNING' ||
normalized === 'WAITING'
) {
return 'RUNNING';
}
if (normalized === 'SUSPENDED') {
return 'PAUSED';
}
if (normalized === 'SUCCESS' || normalized === 'ERROR' || normalized === 'CANCELLED') {
return 'FINAL';
}
return null;
}
export function normalizeExecutionStatus(status: string | null | undefined): TaskExecutionStatus {
const normalized = String(status ?? '').toUpperCase();
if (normalized === 'CREATED') return 'CREATED';
if (normalized === 'READY') return 'READY';
if (normalized === 'RUNNING') return 'RUNNING';
if (
normalized === 'WAITING' ||
normalized === 'WAITING_FOR_INPUT' ||
normalized === 'WAITING_FOR_INTERACTION' ||
normalized === 'WAITING_FOR_SUBFLOW' ||
normalized === 'WAITING_FOR_DEPENDENCY'
) {
return 'WAITING';
}
if (normalized === 'SUSPENDED') return 'SUSPENDED';
if (normalized === 'CANCELLED') return 'CANCELLED';
if (normalized === 'SUCCESS' || normalized === 'COMPLETED') return 'SUCCESS';
if (normalized === 'ERROR' || normalized === 'FAILED') return 'ERROR';
return 'CREATED';
}
export function getTaskExecutionStepNode(step: TaskExecutionStep | null | undefined): FlowNode | null {
if (!step) return null;
if (step.node) {
return step.node.nodeFamily === 'container'
? { ...step.node, nodeFamily: 'container' }
: { ...step.node, nodeFamily: 'block' };
}
return null;
}