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:
David Kaya
2026-04-05 21:59:16 +02:00
co-authored by Copilot
parent e47835c1e8
commit 81eb8f7c82
9 changed files with 122 additions and 371 deletions
+4 -8
View File
@@ -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:{&quot;value&quot;: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:&lt;text&gt;</code> Emit literal text
</li>
<li>
<code className="font-mono">return-json:&lt;json&gt;</code> Emit parsed JSON
</li>
<li>
<code className="font-mono">state:set:&lt;scope&gt;:&lt;key&gt;:&lt;json&gt;</code> Set state value
</li>
<li>
<code className="font-mono">state:get:&lt;scope&gt;:&lt;key&gt;</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>
);
}
@@ -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' },
], ],
}, },
{ {
+2 -4
View File
@@ -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':
+20 -46
View File
@@ -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');
+22 -37
View File
@@ -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);