mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-27 21:33:58 +02:00
refactor(workflows): remove code-executor and align function node with Agent Framework
Remove the invented code-executor node type entirely. Rename function-executor to invoke-function to match Agent Framework's InvokeFunctionTool declarative action. Update InvokeFunctionConfig to use functionName, arguments, requireApproval, and resultVariable properties matching the upstream schema. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -97,10 +97,8 @@ function defaultConfigForKind(kind: WorkflowNodeKind): WorkflowNodeConfig {
|
|||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
reasoningEffort: 'high',
|
reasoningEffort: 'high',
|
||||||
};
|
};
|
||||||
case 'code-executor':
|
case 'invoke-function':
|
||||||
return { kind: 'code-executor' };
|
return { kind: 'invoke-function', functionName: '', arguments: {} };
|
||||||
case 'function-executor':
|
|
||||||
return { kind: 'function-executor', functionRef: '' };
|
|
||||||
case 'sub-workflow':
|
case 'sub-workflow':
|
||||||
return { kind: 'sub-workflow' };
|
return { kind: 'sub-workflow' };
|
||||||
case 'request-port':
|
case 'request-port':
|
||||||
@@ -116,10 +114,8 @@ function defaultLabelForKind(kind: WorkflowNodeKind): string {
|
|||||||
return 'End';
|
return 'End';
|
||||||
case 'agent':
|
case 'agent':
|
||||||
return 'New Agent';
|
return 'New Agent';
|
||||||
case 'code-executor':
|
case 'invoke-function':
|
||||||
return 'Code Executor';
|
return 'Function Tool';
|
||||||
case 'function-executor':
|
|
||||||
return 'Function';
|
|
||||||
case 'sub-workflow':
|
case 'sub-workflow':
|
||||||
return 'Sub-Workflow';
|
return 'Sub-Workflow';
|
||||||
case 'request-port':
|
case 'request-port':
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { AlertCircle, Code, Trash2 } from 'lucide-react';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CodeExecutorConfig,
|
|
||||||
WorkflowNode,
|
|
||||||
WorkflowNodeConfig,
|
|
||||||
WorkflowValidationIssue,
|
|
||||||
} from '@shared/domain/workflow';
|
|
||||||
|
|
||||||
interface CodeExecutorInspectorProps {
|
|
||||||
node: WorkflowNode;
|
|
||||||
validationIssues?: WorkflowValidationIssue[];
|
|
||||||
onNodeChange: (nodeId: string, patch: Partial<WorkflowNode>) => void;
|
|
||||||
onNodeConfigChange: (nodeId: string, config: WorkflowNodeConfig) => void;
|
|
||||||
onNodeRemove: (nodeId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputField({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
multiline,
|
|
||||||
placeholder,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
multiline?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
}) {
|
|
||||||
const base =
|
|
||||||
'w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-3 py-2 text-[13px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition focus:border-[var(--color-accent)]/50';
|
|
||||||
return (
|
|
||||||
<label className="block space-y-1.5">
|
|
||||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">{label}</span>
|
|
||||||
{multiline ? (
|
|
||||||
<textarea
|
|
||||||
className={`${base} min-h-20 resize-y`}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
className={base}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CodeExecutorInspector({
|
|
||||||
node,
|
|
||||||
validationIssues,
|
|
||||||
onNodeChange,
|
|
||||||
onNodeConfigChange,
|
|
||||||
onNodeRemove,
|
|
||||||
}: CodeExecutorInspectorProps) {
|
|
||||||
const config = node.config as CodeExecutorConfig;
|
|
||||||
const nodeIssues = validationIssues?.filter((i) => i.nodeId === node.id) ?? [];
|
|
||||||
|
|
||||||
const patchConfig = useCallback(
|
|
||||||
(patch: Partial<CodeExecutorConfig>) => {
|
|
||||||
onNodeConfigChange(node.id, { ...config, ...patch });
|
|
||||||
},
|
|
||||||
[node.id, config, onNodeConfigChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<div className="flex size-8 items-center justify-center rounded-lg bg-sky-500/10">
|
|
||||||
<Code className="size-4 text-sky-400" />
|
|
||||||
</div>
|
|
||||||
<div className="text-[13px] font-semibold text-[var(--color-text-primary)]">
|
|
||||||
{node.label || 'Code Executor'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="flex size-7 items-center justify-center rounded-lg text-[var(--color-text-muted)] transition-all duration-200 hover:bg-[var(--color-status-error)]/10 hover:text-[var(--color-status-error)]"
|
|
||||||
onClick={() => onNodeRemove(node.id)}
|
|
||||||
title="Remove node"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Trash2 className="size-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Label */}
|
|
||||||
<InputField
|
|
||||||
label="Label"
|
|
||||||
onChange={(v) => onNodeChange(node.id, { label: v })}
|
|
||||||
placeholder="Display label"
|
|
||||||
value={node.label}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Implementation */}
|
|
||||||
<label className="block space-y-1.5">
|
|
||||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
|
|
||||||
Implementation
|
|
||||||
</span>
|
|
||||||
<textarea
|
|
||||||
className="w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-3 py-2 font-mono text-[13px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition focus:border-[var(--color-accent)]/50 min-h-20 resize-y"
|
|
||||||
onChange={(e) => patchConfig({ implementation: e.target.value })}
|
|
||||||
placeholder="e.g. return-input, return-text:hello, state:set:scope:key:{"value":1}"
|
|
||||||
value={config.implementation ?? ''}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Directives help */}
|
|
||||||
<div className="rounded-lg border border-sky-500/20 bg-sky-500/5 px-3 py-2 text-[12px] text-sky-400">
|
|
||||||
<div className="mb-1.5 font-medium">Supported directives</div>
|
|
||||||
<ul className="space-y-0.5 text-[11px]">
|
|
||||||
<li>
|
|
||||||
<code className="font-mono">return-input</code> — Forward incoming payload
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="font-mono">return-text:<text></code> — Emit literal text
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="font-mono">return-json:<json></code> — Emit parsed JSON
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="font-mono">state:set:<scope>:<key>:<json></code> — Set state value
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="font-mono">state:get:<scope>:<key></code> — Read state value
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Input Type */}
|
|
||||||
<InputField
|
|
||||||
label="Input Type"
|
|
||||||
onChange={(v) => patchConfig({ inputType: v || undefined })}
|
|
||||||
placeholder="Optional type annotation"
|
|
||||||
value={config.inputType ?? ''}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Output Type */}
|
|
||||||
<InputField
|
|
||||||
label="Output Type"
|
|
||||||
onChange={(v) => patchConfig({ outputType: v || undefined })}
|
|
||||||
placeholder="Optional type annotation"
|
|
||||||
value={config.outputType ?? ''}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Validation issues */}
|
|
||||||
{nodeIssues.length > 0 && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{nodeIssues.map((issue, i) => (
|
|
||||||
<div
|
|
||||||
className={`flex items-start gap-1.5 rounded-lg px-2.5 py-1.5 text-[11px] ${
|
|
||||||
issue.level === 'error'
|
|
||||||
? 'bg-[var(--color-status-error)]/10 text-[var(--color-status-error)]'
|
|
||||||
: 'bg-[var(--color-status-warning)]/10 text-[var(--color-status-warning)]'
|
|
||||||
}`}
|
|
||||||
key={`${issue.field ?? 'v'}-${i}`}
|
|
||||||
>
|
|
||||||
<AlertCircle className="mt-0.5 size-3 shrink-0" />
|
|
||||||
<span>{issue.message}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+62
-58
@@ -2,13 +2,13 @@ import { useCallback, useState } from 'react';
|
|||||||
import { AlertCircle, FunctionSquare, Plus, Trash2, X } from 'lucide-react';
|
import { AlertCircle, FunctionSquare, Plus, Trash2, X } from 'lucide-react';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FunctionExecutorConfig,
|
InvokeFunctionConfig,
|
||||||
WorkflowNode,
|
WorkflowNode,
|
||||||
WorkflowNodeConfig,
|
WorkflowNodeConfig,
|
||||||
WorkflowValidationIssue,
|
WorkflowValidationIssue,
|
||||||
} from '@shared/domain/workflow';
|
} from '@shared/domain/workflow';
|
||||||
|
|
||||||
interface FunctionExecutorInspectorProps {
|
interface InvokeFunctionInspectorProps {
|
||||||
node: WorkflowNode;
|
node: WorkflowNode;
|
||||||
validationIssues?: WorkflowValidationIssue[];
|
validationIssues?: WorkflowValidationIssue[];
|
||||||
onNodeChange: (nodeId: string, patch: Partial<WorkflowNode>) => void;
|
onNodeChange: (nodeId: string, patch: Partial<WorkflowNode>) => void;
|
||||||
@@ -42,66 +42,58 @@ function InputField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const builtInFunctions = [
|
|
||||||
{ value: 'identity', label: 'identity — Pass through input' },
|
|
||||||
{ value: 'return-parameter', label: 'return-parameter — Return a parameter value' },
|
|
||||||
{ value: 'concat-text', label: 'concat-text — Concatenate text values' },
|
|
||||||
{ value: 'state:get', label: 'state:get — Read state value' },
|
|
||||||
{ value: 'state:set', label: 'state:set — Set state value' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
function stringifyValue(v: unknown): string {
|
function stringifyValue(v: unknown): string {
|
||||||
if (typeof v === 'string') return v;
|
if (typeof v === 'string') return v;
|
||||||
return JSON.stringify(v) ?? '';
|
return JSON.stringify(v) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FunctionExecutorInspector({
|
export function InvokeFunctionInspector({
|
||||||
node,
|
node,
|
||||||
validationIssues,
|
validationIssues,
|
||||||
onNodeChange,
|
onNodeChange,
|
||||||
onNodeConfigChange,
|
onNodeConfigChange,
|
||||||
onNodeRemove,
|
onNodeRemove,
|
||||||
}: FunctionExecutorInspectorProps) {
|
}: InvokeFunctionInspectorProps) {
|
||||||
const config = node.config as FunctionExecutorConfig;
|
const config = node.config as InvokeFunctionConfig;
|
||||||
const nodeIssues = validationIssues?.filter((i) => i.nodeId === node.id) ?? [];
|
const nodeIssues = validationIssues?.filter((i) => i.nodeId === node.id) ?? [];
|
||||||
const parameters = config.parameters ?? {};
|
const args = config.arguments ?? {};
|
||||||
const paramEntries = Object.entries(parameters);
|
const argEntries = Object.entries(args);
|
||||||
|
|
||||||
const [newKey, setNewKey] = useState('');
|
const [newKey, setNewKey] = useState('');
|
||||||
|
|
||||||
const patchConfig = useCallback(
|
const patchConfig = useCallback(
|
||||||
(patch: Partial<FunctionExecutorConfig>) => {
|
(patch: Partial<InvokeFunctionConfig>) => {
|
||||||
onNodeConfigChange(node.id, { ...config, ...patch });
|
onNodeConfigChange(node.id, { ...config, ...patch });
|
||||||
},
|
},
|
||||||
[node.id, config, onNodeConfigChange],
|
[node.id, config, onNodeConfigChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleParamChange = useCallback(
|
const handleArgChange = useCallback(
|
||||||
(oldKey: string, newParamKey: string, value: string) => {
|
(oldKey: string, newArgKey: string, value: string) => {
|
||||||
const next = { ...parameters };
|
const next = { ...args };
|
||||||
if (newParamKey !== oldKey) {
|
if (newArgKey !== oldKey) {
|
||||||
delete next[oldKey];
|
delete next[oldKey];
|
||||||
}
|
}
|
||||||
next[newParamKey] = value;
|
next[newArgKey] = value;
|
||||||
patchConfig({ parameters: next });
|
patchConfig({ arguments: next });
|
||||||
},
|
},
|
||||||
[parameters, patchConfig],
|
[args, patchConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleParamRemove = useCallback(
|
const handleArgRemove = useCallback(
|
||||||
(key: string) => {
|
(key: string) => {
|
||||||
const next = { ...parameters };
|
const next = { ...args };
|
||||||
delete next[key];
|
delete next[key];
|
||||||
patchConfig({ parameters: next });
|
patchConfig({ arguments: next });
|
||||||
},
|
},
|
||||||
[parameters, patchConfig],
|
[args, patchConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleParamAdd = useCallback(() => {
|
const handleArgAdd = useCallback(() => {
|
||||||
const key = newKey.trim() || `param${paramEntries.length + 1}`;
|
const key = newKey.trim() || `arg${argEntries.length + 1}`;
|
||||||
patchConfig({ parameters: { ...parameters, [key]: '' } });
|
patchConfig({ arguments: { ...args, [key]: '' } });
|
||||||
setNewKey('');
|
setNewKey('');
|
||||||
}, [newKey, paramEntries.length, parameters, patchConfig]);
|
}, [newKey, argEntries.length, args, patchConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -112,7 +104,7 @@ export function FunctionExecutorInspector({
|
|||||||
<FunctionSquare className="size-4 text-violet-400" />
|
<FunctionSquare className="size-4 text-violet-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[13px] font-semibold text-[var(--color-text-primary)]">
|
<div className="text-[13px] font-semibold text-[var(--color-text-primary)]">
|
||||||
{node.label || 'Function Executor'}
|
{node.label || 'Function Tool'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -133,61 +125,73 @@ export function FunctionExecutorInspector({
|
|||||||
value={node.label}
|
value={node.label}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Function reference */}
|
{/* Function name */}
|
||||||
<label className="block space-y-1.5">
|
<InputField
|
||||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">Function</span>
|
label="Function Name"
|
||||||
<select
|
onChange={(v) => patchConfig({ functionName: v })}
|
||||||
className="w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-3 py-2 text-[13px] text-[var(--color-text-primary)] outline-none transition focus:border-[var(--color-accent)]/50"
|
placeholder="e.g. GetUserData"
|
||||||
onChange={(e) => patchConfig({ functionRef: e.target.value })}
|
value={config.functionName}
|
||||||
value={config.functionRef}
|
/>
|
||||||
>
|
|
||||||
<option value="">Select a function…</option>
|
{/* Result variable */}
|
||||||
{builtInFunctions.map((fn) => (
|
<InputField
|
||||||
<option key={fn.value} value={fn.value}>
|
label="Result Variable"
|
||||||
{fn.label}
|
onChange={(v) => patchConfig({ resultVariable: v || undefined })}
|
||||||
</option>
|
placeholder="e.g. Local.result"
|
||||||
))}
|
value={config.resultVariable ?? ''}
|
||||||
</select>
|
/>
|
||||||
|
|
||||||
|
{/* Require approval */}
|
||||||
|
<label className="flex items-center justify-between">
|
||||||
|
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
|
||||||
|
Require Approval
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
checked={config.requireApproval === true}
|
||||||
|
className="size-4 accent-[var(--color-accent)]"
|
||||||
|
onChange={(e) => patchConfig({ requireApproval: e.target.checked || undefined })}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* Parameters editor */}
|
{/* Arguments editor */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
|
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
|
||||||
Parameters
|
Arguments
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
className="flex size-6 items-center justify-center rounded-md text-[var(--color-text-muted)] transition-all duration-200 hover:bg-[var(--color-accent)]/10 hover:text-[var(--color-accent)]"
|
className="flex size-6 items-center justify-center rounded-md text-[var(--color-text-muted)] transition-all duration-200 hover:bg-[var(--color-accent)]/10 hover:text-[var(--color-accent)]"
|
||||||
onClick={handleParamAdd}
|
onClick={handleArgAdd}
|
||||||
title="Add parameter"
|
title="Add argument"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<Plus className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paramEntries.length === 0 && (
|
{argEntries.length === 0 && (
|
||||||
<p className="text-[11px] text-[var(--color-text-muted)]">No parameters defined.</p>
|
<p className="text-[11px] text-[var(--color-text-muted)]">No arguments defined.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{paramEntries.map(([key, value]) => (
|
{argEntries.map(([key, value]) => (
|
||||||
<div className="flex items-center gap-1.5" key={key}>
|
<div className="flex items-center gap-1.5" key={key}>
|
||||||
<input
|
<input
|
||||||
className="w-1/3 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-2 py-1.5 text-[12px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition focus:border-[var(--color-accent)]/50"
|
className="w-1/3 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-2 py-1.5 text-[12px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition focus:border-[var(--color-accent)]/50"
|
||||||
onChange={(e) => handleParamChange(key, e.target.value, stringifyValue(value))}
|
onChange={(e) => handleArgChange(key, e.target.value, stringifyValue(value))}
|
||||||
placeholder="key"
|
placeholder="key"
|
||||||
value={key}
|
value={key}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="flex-1 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-2 py-1.5 text-[12px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition focus:border-[var(--color-accent)]/50"
|
className="flex-1 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-2 py-1.5 text-[12px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition focus:border-[var(--color-accent)]/50"
|
||||||
onChange={(e) => handleParamChange(key, key, e.target.value)}
|
onChange={(e) => handleArgChange(key, key, e.target.value)}
|
||||||
placeholder="value"
|
placeholder="value"
|
||||||
value={stringifyValue(value)}
|
value={stringifyValue(value)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="flex size-6 shrink-0 items-center justify-center rounded-md text-[var(--color-text-muted)] transition-all duration-200 hover:bg-[var(--color-status-error)]/10 hover:text-[var(--color-status-error)]"
|
className="flex size-6 shrink-0 items-center justify-center rounded-md text-[var(--color-text-muted)] transition-all duration-200 hover:bg-[var(--color-status-error)]/10 hover:text-[var(--color-status-error)]"
|
||||||
onClick={() => handleParamRemove(key)}
|
onClick={() => handleArgRemove(key)}
|
||||||
title="Remove parameter"
|
title="Remove argument"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<X className="size-3" />
|
<X className="size-3" />
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { AlertCircle, Bot, Code, FunctionSquare, GitBranch, Info, Radio, Trash2 } from 'lucide-react';
|
import { AlertCircle, Bot, FunctionSquare, GitBranch, Info, Radio, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findModel,
|
findModel,
|
||||||
@@ -17,9 +17,8 @@ import type {
|
|||||||
AgentNodeConfig,
|
AgentNodeConfig,
|
||||||
} from '@shared/domain/workflow';
|
} from '@shared/domain/workflow';
|
||||||
import { ModelSelect, ReasoningEffortSelect } from '@renderer/components/AgentConfigFields';
|
import { ModelSelect, ReasoningEffortSelect } from '@renderer/components/AgentConfigFields';
|
||||||
import { CodeExecutorInspector } from '@renderer/components/workflow/CodeExecutorInspector';
|
import { InvokeFunctionInspector } from '@renderer/components/workflow/InvokeFunctionInspector';
|
||||||
import { ConditionEditor } from '@renderer/components/workflow/ConditionEditor';
|
import { ConditionEditor } from '@renderer/components/workflow/ConditionEditor';
|
||||||
import { FunctionExecutorInspector } from '@renderer/components/workflow/FunctionExecutorInspector';
|
|
||||||
import { RequestPortInspector } from '@renderer/components/workflow/RequestPortInspector';
|
import { RequestPortInspector } from '@renderer/components/workflow/RequestPortInspector';
|
||||||
import { SubWorkflowInspector } from '@renderer/components/workflow/SubWorkflowInspector';
|
import { SubWorkflowInspector } from '@renderer/components/workflow/SubWorkflowInspector';
|
||||||
|
|
||||||
@@ -194,9 +193,7 @@ function SystemNodeInspector({ node }: { node: WorkflowNode }) {
|
|||||||
|
|
||||||
/* ── Placeholder inspector for future node kinds ───────────── */
|
/* ── Placeholder inspector for future node kinds ───────────── */
|
||||||
|
|
||||||
const placeholderIcons: Record<string, typeof Code> = {
|
const placeholderIcons: Record<string, typeof FunctionSquare> = {
|
||||||
'code-executor': Code,
|
|
||||||
'function-executor': FunctionSquare,
|
|
||||||
'sub-workflow': GitBranch,
|
'sub-workflow': GitBranch,
|
||||||
'request-port': Radio,
|
'request-port': Radio,
|
||||||
};
|
};
|
||||||
@@ -460,24 +457,10 @@ export function WorkflowGraphInspector({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedNode.kind === 'code-executor') {
|
if (selectedNode.kind === 'invoke-function') {
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<CodeExecutorInspector
|
<InvokeFunctionInspector
|
||||||
node={selectedNode}
|
|
||||||
validationIssues={validationIssues}
|
|
||||||
onNodeChange={onNodeChange}
|
|
||||||
onNodeConfigChange={onNodeConfigChange}
|
|
||||||
onNodeRemove={onNodeRemove}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedNode.kind === 'function-executor') {
|
|
||||||
return (
|
|
||||||
<div className="p-4">
|
|
||||||
<FunctionExecutorInspector
|
|
||||||
node={selectedNode}
|
node={selectedNode}
|
||||||
validationIssues={validationIssues}
|
validationIssues={validationIssues}
|
||||||
onNodeChange={onNodeChange}
|
onNodeChange={onNodeChange}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Handle, Position, type NodeProps } from '@xyflow/react';
|
import { Handle, Position, type NodeProps } from '@xyflow/react';
|
||||||
import { Bot, Circle, Code, FunctionSquare, GitBranch, Link2, Radio, Play, Square } from 'lucide-react';
|
import { Bot, Circle, FunctionSquare, GitBranch, Link2, Radio, Play, Square } from 'lucide-react';
|
||||||
|
|
||||||
import type { WorkflowGraphNodeData } from '@renderer/lib/workflowGraph';
|
import type { WorkflowGraphNodeData } from '@renderer/lib/workflowGraph';
|
||||||
import type { WorkflowNodeKind } from '@shared/domain/workflow';
|
import type { WorkflowNodeKind } from '@shared/domain/workflow';
|
||||||
@@ -12,8 +12,7 @@ const kindColors: Record<WorkflowNodeKind, { bg: string; border: string; text: s
|
|||||||
start: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', text: 'text-emerald-400' },
|
start: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', text: 'text-emerald-400' },
|
||||||
end: { bg: 'bg-rose-500/10', border: 'border-rose-500/30', text: 'text-rose-400' },
|
end: { bg: 'bg-rose-500/10', border: 'border-rose-500/30', text: 'text-rose-400' },
|
||||||
agent: { bg: 'bg-[var(--color-surface-2)]/80', border: 'border-[var(--color-border)]/40', text: 'text-[var(--color-text-primary)]' },
|
agent: { bg: 'bg-[var(--color-surface-2)]/80', border: 'border-[var(--color-border)]/40', text: 'text-[var(--color-text-primary)]' },
|
||||||
'code-executor': { bg: 'bg-sky-500/10', border: 'border-sky-500/30', text: 'text-sky-400' },
|
'invoke-function': { bg: 'bg-violet-500/10', border: 'border-violet-500/30', text: 'text-violet-400' },
|
||||||
'function-executor': { bg: 'bg-violet-500/10', border: 'border-violet-500/30', text: 'text-violet-400' },
|
|
||||||
'sub-workflow': { bg: 'bg-amber-500/10', border: 'border-amber-500/30', text: 'text-amber-400' },
|
'sub-workflow': { bg: 'bg-amber-500/10', border: 'border-amber-500/30', text: 'text-amber-400' },
|
||||||
'request-port': { bg: 'bg-teal-500/10', border: 'border-teal-500/30', text: 'text-teal-400' },
|
'request-port': { bg: 'bg-teal-500/10', border: 'border-teal-500/30', text: 'text-teal-400' },
|
||||||
};
|
};
|
||||||
@@ -22,8 +21,7 @@ const kindIcons: Record<WorkflowNodeKind, typeof Bot> = {
|
|||||||
start: Play,
|
start: Play,
|
||||||
end: Square,
|
end: Square,
|
||||||
agent: Bot,
|
agent: Bot,
|
||||||
'code-executor': Code,
|
'invoke-function': FunctionSquare,
|
||||||
'function-executor': FunctionSquare,
|
|
||||||
'sub-workflow': GitBranch,
|
'sub-workflow': GitBranch,
|
||||||
'request-port': Radio,
|
'request-port': Radio,
|
||||||
};
|
};
|
||||||
@@ -115,18 +113,7 @@ export const AgentNode = memo(function AgentNode({ data, selected }: NodeProps)
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CodeExecutorNode = memo(function CodeExecutorNode({ data, selected }: NodeProps) {
|
export const InvokeFunctionNode = memo(function InvokeFunctionNode({ data, selected }: NodeProps) {
|
||||||
const nodeData = data as unknown as WorkflowGraphNodeData;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Handle type="target" position={Position.Left} className={handleStyles.flow} />
|
|
||||||
<WorkflowNodeContent data={nodeData} selected={selected ?? false} />
|
|
||||||
<Handle type="source" position={Position.Right} className={handleStyles.flow} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const FunctionExecutorNode = memo(function FunctionExecutorNode({ data, selected }: NodeProps) {
|
|
||||||
const nodeData = data as unknown as WorkflowGraphNodeData;
|
const nodeData = data as unknown as WorkflowGraphNodeData;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -165,8 +152,7 @@ export const workflowNodeTypes = {
|
|||||||
startNode: StartNode,
|
startNode: StartNode,
|
||||||
endNode: EndNode,
|
endNode: EndNode,
|
||||||
agentNode: AgentNode,
|
agentNode: AgentNode,
|
||||||
codeExecutorNode: CodeExecutorNode,
|
invokeFunctionNode: InvokeFunctionNode,
|
||||||
functionExecutorNode: FunctionExecutorNode,
|
|
||||||
subWorkflowNode: SubWorkflowNode,
|
subWorkflowNode: SubWorkflowNode,
|
||||||
requestPortNode: RequestPortNode,
|
requestPortNode: RequestPortNode,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Bot, Code, FunctionSquare, GitBranch, Play, Radio, Square } from 'lucide-react';
|
import { Bot, FunctionSquare, GitBranch, Play, Radio, Square } from 'lucide-react';
|
||||||
|
|
||||||
import type { WorkflowNodeKind } from '@shared/domain/workflow';
|
import type { WorkflowNodeKind } from '@shared/domain/workflow';
|
||||||
|
|
||||||
@@ -36,8 +36,7 @@ const paletteGroups: PaletteGroup[] = [
|
|||||||
{
|
{
|
||||||
label: 'Processing',
|
label: 'Processing',
|
||||||
items: [
|
items: [
|
||||||
{ kind: 'code-executor', label: 'Code', icon: Code, color: 'text-sky-400' },
|
{ kind: 'invoke-function', label: 'Function Tool', icon: FunctionSquare, color: 'text-violet-400' },
|
||||||
{ kind: 'function-executor', label: 'Function', icon: FunctionSquare, color: 'text-violet-400' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,10 +36,8 @@ function resolveNodeType(kind: WorkflowNodeKind): string {
|
|||||||
return 'endNode';
|
return 'endNode';
|
||||||
case 'agent':
|
case 'agent':
|
||||||
return 'agentNode';
|
return 'agentNode';
|
||||||
case 'code-executor':
|
case 'invoke-function':
|
||||||
return 'codeExecutorNode';
|
return 'invokeFunctionNode';
|
||||||
case 'function-executor':
|
|
||||||
return 'functionExecutorNode';
|
|
||||||
case 'sub-workflow':
|
case 'sub-workflow':
|
||||||
return 'subWorkflowNode';
|
return 'subWorkflowNode';
|
||||||
case 'request-port':
|
case 'request-port':
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ export type WorkflowNodeKind =
|
|||||||
| 'start'
|
| 'start'
|
||||||
| 'end'
|
| 'end'
|
||||||
| 'agent'
|
| 'agent'
|
||||||
| 'code-executor'
|
| 'invoke-function'
|
||||||
| 'function-executor'
|
|
||||||
| 'sub-workflow'
|
| 'sub-workflow'
|
||||||
| 'request-port';
|
| 'request-port';
|
||||||
|
|
||||||
@@ -61,17 +60,12 @@ export interface AgentNodeConfig extends PatternAgentDefinition {
|
|||||||
kind: 'agent';
|
kind: 'agent';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CodeExecutorConfig {
|
export interface InvokeFunctionConfig {
|
||||||
kind: 'code-executor';
|
kind: 'invoke-function';
|
||||||
inputType?: string;
|
functionName: string;
|
||||||
outputType?: string;
|
arguments?: Record<string, unknown>;
|
||||||
implementation?: string;
|
requireApproval?: boolean;
|
||||||
}
|
resultVariable?: string;
|
||||||
|
|
||||||
export interface FunctionExecutorConfig {
|
|
||||||
kind: 'function-executor';
|
|
||||||
functionRef: string;
|
|
||||||
parameters?: Record<string, unknown>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubWorkflowConfig {
|
export interface SubWorkflowConfig {
|
||||||
@@ -92,8 +86,7 @@ export type WorkflowNodeConfig =
|
|||||||
| StartNodeConfig
|
| StartNodeConfig
|
||||||
| EndNodeConfig
|
| EndNodeConfig
|
||||||
| AgentNodeConfig
|
| AgentNodeConfig
|
||||||
| CodeExecutorConfig
|
| InvokeFunctionConfig
|
||||||
| FunctionExecutorConfig
|
|
||||||
| SubWorkflowConfig
|
| SubWorkflowConfig
|
||||||
| RequestPortConfig;
|
| RequestPortConfig;
|
||||||
|
|
||||||
@@ -174,8 +167,7 @@ const executableNodeKinds = new Set<WorkflowNodeKind>([
|
|||||||
'start',
|
'start',
|
||||||
'end',
|
'end',
|
||||||
'agent',
|
'agent',
|
||||||
'code-executor',
|
'invoke-function',
|
||||||
'function-executor',
|
|
||||||
'sub-workflow',
|
'sub-workflow',
|
||||||
'request-port',
|
'request-port',
|
||||||
]);
|
]);
|
||||||
@@ -219,21 +211,14 @@ function normalizeNodeConfig(kind: WorkflowNodeKind, config?: Partial<WorkflowNo
|
|||||||
overrides: agent?.overrides,
|
overrides: agent?.overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'code-executor': {
|
case 'invoke-function': {
|
||||||
const value = config as Partial<CodeExecutorConfig> | undefined;
|
const value = config as Partial<InvokeFunctionConfig> | undefined;
|
||||||
return {
|
return {
|
||||||
kind,
|
kind,
|
||||||
inputType: normalizeOptionalString(value?.inputType),
|
functionName: normalizeOptionalString(value?.functionName) ?? '',
|
||||||
outputType: normalizeOptionalString(value?.outputType),
|
arguments: value?.arguments,
|
||||||
implementation: value?.implementation?.trim(),
|
requireApproval: value?.requireApproval,
|
||||||
};
|
resultVariable: normalizeOptionalString(value?.resultVariable),
|
||||||
}
|
|
||||||
case 'function-executor': {
|
|
||||||
const value = config as Partial<FunctionExecutorConfig> | undefined;
|
|
||||||
return {
|
|
||||||
kind,
|
|
||||||
functionRef: normalizeOptionalString(value?.functionRef) ?? '',
|
|
||||||
parameters: value?.parameters,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'sub-workflow': {
|
case 'sub-workflow': {
|
||||||
@@ -750,23 +735,13 @@ function validateSubWorkflowNode(node: WorkflowNode, issues: WorkflowValidationI
|
|||||||
|
|
||||||
function validateExecutableNodeConfig(node: WorkflowNode, issues: WorkflowValidationIssue[]): void {
|
function validateExecutableNodeConfig(node: WorkflowNode, issues: WorkflowValidationIssue[]): void {
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case 'code-executor':
|
case 'invoke-function':
|
||||||
if (node.config.kind === 'code-executor' && !node.config.implementation?.trim()) {
|
if (node.config.kind === 'invoke-function' && !node.config.functionName.trim()) {
|
||||||
addIssue(issues, {
|
addIssue(issues, {
|
||||||
level: 'error',
|
level: 'error',
|
||||||
field: 'graph.nodes.config.implementation',
|
field: 'graph.nodes.config.functionName',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
message: 'Code executor nodes require a non-empty implementation.',
|
message: 'Function tool nodes require a non-empty functionName.',
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case 'function-executor':
|
|
||||||
if (node.config.kind === 'function-executor' && !node.config.functionRef.trim()) {
|
|
||||||
addIssue(issues, {
|
|
||||||
level: 'error',
|
|
||||||
field: 'graph.nodes.config.functionRef',
|
|
||||||
nodeId: node.id,
|
|
||||||
message: 'Function executor nodes require a non-empty functionRef.',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -930,8 +905,7 @@ export function validateWorkflowDefinition(workflow: WorkflowDefinition): Workfl
|
|||||||
const endNodes = normalized.graph.nodes.filter((node) => node.kind === 'end');
|
const endNodes = normalized.graph.nodes.filter((node) => node.kind === 'end');
|
||||||
const executableWorkNodes = normalized.graph.nodes.filter((node) =>
|
const executableWorkNodes = normalized.graph.nodes.filter((node) =>
|
||||||
node.kind === 'agent'
|
node.kind === 'agent'
|
||||||
|| node.kind === 'code-executor'
|
|| node.kind === 'invoke-function'
|
||||||
|| node.kind === 'function-executor'
|
|
||||||
|| node.kind === 'sub-workflow'
|
|| node.kind === 'sub-workflow'
|
||||||
|| node.kind === 'request-port');
|
|| node.kind === 'request-port');
|
||||||
|
|
||||||
|
|||||||
@@ -119,17 +119,17 @@ describe('workflow validation', () => {
|
|||||||
test('accepts phase 4 executor node kinds as executable work', () => {
|
test('accepts phase 4 executor node kinds as executable work', () => {
|
||||||
const workflow = createWorkflow();
|
const workflow = createWorkflow();
|
||||||
workflow.graph.nodes[1] = {
|
workflow.graph.nodes[1] = {
|
||||||
id: 'code-executor',
|
id: 'invoke-function',
|
||||||
kind: 'code-executor',
|
kind: 'invoke-function',
|
||||||
label: 'Transform',
|
label: 'Function Tool',
|
||||||
position: { x: 100, y: 0 },
|
position: { x: 100, y: 0 },
|
||||||
config: {
|
config: {
|
||||||
kind: 'code-executor',
|
kind: 'invoke-function',
|
||||||
implementation: 'return-text:done',
|
functionName: 'GetUserData',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
workflow.graph.edges[0] = { id: 'edge-start-code', source: 'start', target: 'code-executor', kind: 'direct' };
|
workflow.graph.edges[0] = { id: 'edge-start-fn', source: 'start', target: 'invoke-function', kind: 'direct' };
|
||||||
workflow.graph.edges[1] = { id: 'edge-code-end', source: 'code-executor', target: 'end', kind: 'direct' };
|
workflow.graph.edges[1] = { id: 'edge-fn-end', source: 'invoke-function', target: 'end', kind: 'direct' };
|
||||||
|
|
||||||
expect(validateWorkflowDefinition(workflow)).toEqual([]);
|
expect(validateWorkflowDefinition(workflow)).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -137,17 +137,17 @@ describe('workflow validation', () => {
|
|||||||
test('counts function and request port nodes as executable work', () => {
|
test('counts function and request port nodes as executable work', () => {
|
||||||
const functionWorkflow = createWorkflow();
|
const functionWorkflow = createWorkflow();
|
||||||
functionWorkflow.graph.nodes[1] = {
|
functionWorkflow.graph.nodes[1] = {
|
||||||
id: 'function-executor',
|
id: 'invoke-function',
|
||||||
kind: 'function-executor',
|
kind: 'invoke-function',
|
||||||
label: 'Function',
|
label: 'Function Tool',
|
||||||
position: { x: 200, y: 0 },
|
position: { x: 200, y: 0 },
|
||||||
config: {
|
config: {
|
||||||
kind: 'function-executor',
|
kind: 'invoke-function',
|
||||||
functionRef: 'identity',
|
functionName: 'identity',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
functionWorkflow.graph.edges[0] = { id: 'edge-start-function', source: 'start', target: 'function-executor', kind: 'direct' };
|
functionWorkflow.graph.edges[0] = { id: 'edge-start-function', source: 'start', target: 'invoke-function', kind: 'direct' };
|
||||||
functionWorkflow.graph.edges[1] = { id: 'edge-function-end', source: 'function-executor', target: 'end', kind: 'direct' };
|
functionWorkflow.graph.edges[1] = { id: 'edge-function-end', source: 'invoke-function', target: 'end', kind: 'direct' };
|
||||||
|
|
||||||
const requestPortWorkflow = createWorkflow();
|
const requestPortWorkflow = createWorkflow();
|
||||||
requestPortWorkflow.graph.nodes[1] = {
|
requestPortWorkflow.graph.nodes[1] = {
|
||||||
@@ -170,33 +170,19 @@ describe('workflow validation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('rejects invalid phase 4 executor configs', () => {
|
test('rejects invalid phase 4 executor configs', () => {
|
||||||
const codeWorkflow = createWorkflow();
|
|
||||||
codeWorkflow.graph.nodes[1] = {
|
|
||||||
id: 'code-executor',
|
|
||||||
kind: 'code-executor',
|
|
||||||
label: 'Code',
|
|
||||||
position: { x: 200, y: 0 },
|
|
||||||
config: {
|
|
||||||
kind: 'code-executor',
|
|
||||||
implementation: ' ',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
codeWorkflow.graph.edges[0] = { id: 'edge-start-code', source: 'start', target: 'code-executor', kind: 'direct' };
|
|
||||||
codeWorkflow.graph.edges[1] = { id: 'edge-code-end', source: 'code-executor', target: 'end', kind: 'direct' };
|
|
||||||
|
|
||||||
const functionWorkflow = createWorkflow();
|
const functionWorkflow = createWorkflow();
|
||||||
functionWorkflow.graph.nodes[1] = {
|
functionWorkflow.graph.nodes[1] = {
|
||||||
id: 'function-executor',
|
id: 'invoke-function',
|
||||||
kind: 'function-executor',
|
kind: 'invoke-function',
|
||||||
label: 'Function',
|
label: 'Function Tool',
|
||||||
position: { x: 200, y: 0 },
|
position: { x: 200, y: 0 },
|
||||||
config: {
|
config: {
|
||||||
kind: 'function-executor',
|
kind: 'invoke-function',
|
||||||
functionRef: ' ',
|
functionName: ' ',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
functionWorkflow.graph.edges[0] = { id: 'edge-start-function', source: 'start', target: 'function-executor', kind: 'direct' };
|
functionWorkflow.graph.edges[0] = { id: 'edge-start-function', source: 'start', target: 'invoke-function', kind: 'direct' };
|
||||||
functionWorkflow.graph.edges[1] = { id: 'edge-function-end', source: 'function-executor', target: 'end', kind: 'direct' };
|
functionWorkflow.graph.edges[1] = { id: 'edge-function-end', source: 'invoke-function', target: 'end', kind: 'direct' };
|
||||||
|
|
||||||
const requestPortWorkflow = createWorkflow();
|
const requestPortWorkflow = createWorkflow();
|
||||||
requestPortWorkflow.graph.nodes[1] = {
|
requestPortWorkflow.graph.nodes[1] = {
|
||||||
@@ -214,8 +200,7 @@ describe('workflow validation', () => {
|
|||||||
requestPortWorkflow.graph.edges[0] = { id: 'edge-start-port', source: 'start', target: 'request-port', kind: 'direct' };
|
requestPortWorkflow.graph.edges[0] = { id: 'edge-start-port', source: 'start', target: 'request-port', kind: 'direct' };
|
||||||
requestPortWorkflow.graph.edges[1] = { id: 'edge-port-end', source: 'request-port', target: 'end', kind: 'direct' };
|
requestPortWorkflow.graph.edges[1] = { id: 'edge-port-end', source: 'request-port', target: 'end', kind: 'direct' };
|
||||||
|
|
||||||
expect(validateWorkflowDefinition(codeWorkflow).some((issue) => issue.field === 'graph.nodes.config.implementation')).toBe(true);
|
expect(validateWorkflowDefinition(functionWorkflow).some((issue) => issue.field === 'graph.nodes.config.functionName')).toBe(true);
|
||||||
expect(validateWorkflowDefinition(functionWorkflow).some((issue) => issue.field === 'graph.nodes.config.functionRef')).toBe(true);
|
|
||||||
|
|
||||||
const requestPortIssues = validateWorkflowDefinition(requestPortWorkflow);
|
const requestPortIssues = validateWorkflowDefinition(requestPortWorkflow);
|
||||||
expect(requestPortIssues.some((issue) => issue.field === 'graph.nodes.config.portId')).toBe(true);
|
expect(requestPortIssues.some((issue) => issue.field === 'graph.nodes.config.portId')).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user