mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-29 07:58:47 +02:00
fix: synchronize workflow iteration settings across edges, mode settings, and scaffold
For builder-based orchestration modes (group-chat, handoff), the loop edge maxIterations, workflow settings.maxIterations, and mode-specific settings (e.g. groupChat.maxRounds) were independently editable but only the mode settings controlled runtime behavior. This caused confusion when changing one did not update the others. Changes: - Add syncBuilderModeEdgeIterations() to keep loop edge maxIterations in sync with the authoritative mode settings for builder-based modes - Wire sync into normalizeWorkflowDefinition so edges are consistent on load/normalization - Update scaffoldGraphForMode to accept optional settings instead of hardcoded iteration values (4 for handoff, 5 for group-chat) - Sync settings.maxIterations <-> groupChat.maxRounds bidirectionally when either changes in the UI - Make loop edge controls read-only in the graph inspector for builder-based modes with explanatory text - Add 8 consistency tests validating built-in workflows, scaffold behavior, and normalization sync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import type {
|
||||
SubWorkflowConfig,
|
||||
WorkflowStateScope,
|
||||
} from '@shared/domain/workflow';
|
||||
import { validateWorkflowDefinition } from '@shared/domain/workflow';
|
||||
import { validateWorkflowDefinition, isBuilderBasedMode, syncBuilderModeEdgeIterations } from '@shared/domain/workflow';
|
||||
import { createId } from '@shared/utils/ids';
|
||||
import { ToggleSwitch } from '@renderer/components/ui';
|
||||
|
||||
@@ -703,13 +703,30 @@ function WorkflowSettingsPanel({
|
||||
min={1}
|
||||
onChange={(e) => {
|
||||
const raw = parseInt(e.target.value, 10);
|
||||
onChange({
|
||||
const maxIterations = Number.isNaN(raw) ? undefined : raw;
|
||||
const updated: WorkflowDefinition = {
|
||||
...workflow,
|
||||
settings: {
|
||||
...workflow.settings,
|
||||
maxIterations: Number.isNaN(raw) ? undefined : raw,
|
||||
maxIterations,
|
||||
...(workflow.settings.orchestrationMode === 'group-chat' && maxIterations !== undefined
|
||||
? {
|
||||
modeSettings: {
|
||||
...workflow.settings.modeSettings,
|
||||
groupChat: {
|
||||
...(workflow.settings.modeSettings?.groupChat ?? { selectionStrategy: 'round-robin' as const }),
|
||||
maxRounds: maxIterations,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
onChange(
|
||||
isBuilderBasedMode(workflow.settings.orchestrationMode)
|
||||
? syncBuilderModeEdgeIterations(updated)
|
||||
: updated,
|
||||
);
|
||||
}}
|
||||
placeholder="e.g. 5"
|
||||
type="number"
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
isBuilderBasedMode,
|
||||
isGraphBasedMode,
|
||||
scaffoldGraphForMode,
|
||||
syncBuilderModeEdgeIterations,
|
||||
} from '@shared/domain/workflow';
|
||||
import { FormField, InfoCallout, SelectInput, ToggleSwitch } from '@renderer/components/ui';
|
||||
|
||||
@@ -332,23 +333,30 @@ export function OrchestrationModePanel({
|
||||
const applyModeChange = useCallback(
|
||||
(newMode: WorkflowOrchestrationMode, restructure: boolean) => {
|
||||
const modeSettings = createDefaultModeSettings(newMode);
|
||||
const base: WorkflowDefinition = {
|
||||
...workflow,
|
||||
settings: {
|
||||
...workflow.settings,
|
||||
orchestrationMode: newMode,
|
||||
modeSettings: modeSettings
|
||||
? { ...workflow.settings.modeSettings, ...modeSettings }
|
||||
: workflow.settings.modeSettings,
|
||||
},
|
||||
const mergedSettings = {
|
||||
...workflow.settings,
|
||||
orchestrationMode: newMode,
|
||||
modeSettings: modeSettings
|
||||
? { ...workflow.settings.modeSettings, ...modeSettings }
|
||||
: workflow.settings.modeSettings,
|
||||
};
|
||||
|
||||
// Sync maxIterations from mode-specific settings for builder-based modes
|
||||
if (newMode === 'group-chat' && modeSettings?.groupChat) {
|
||||
mergedSettings.maxIterations = modeSettings.groupChat.maxRounds;
|
||||
}
|
||||
|
||||
const base: WorkflowDefinition = { ...workflow, settings: mergedSettings };
|
||||
|
||||
if (restructure) {
|
||||
const existingAgents = workflow.graph.nodes.filter((n) => n.kind === 'agent');
|
||||
const newGraph = scaffoldGraphForMode(newMode, existingAgents.length > 0 ? existingAgents : undefined);
|
||||
const newGraph = scaffoldGraphForMode(newMode, {
|
||||
agentNodes: existingAgents.length > 0 ? existingAgents : undefined,
|
||||
settings: mergedSettings,
|
||||
});
|
||||
onChange({ ...base, graph: newGraph });
|
||||
} else {
|
||||
onChange(base);
|
||||
onChange(syncBuilderModeEdgeIterations(base));
|
||||
}
|
||||
},
|
||||
[workflow, onChange],
|
||||
@@ -387,13 +395,15 @@ export function OrchestrationModePanel({
|
||||
|
||||
const handleGroupChatChange = useCallback(
|
||||
(groupChat: GroupChatModeSettings) => {
|
||||
onChange({
|
||||
const updated: WorkflowDefinition = {
|
||||
...workflow,
|
||||
settings: {
|
||||
...workflow.settings,
|
||||
modeSettings: { ...workflow.settings.modeSettings, groupChat },
|
||||
maxIterations: groupChat.maxRounds,
|
||||
},
|
||||
});
|
||||
};
|
||||
onChange(syncBuilderModeEdgeIterations(updated));
|
||||
},
|
||||
[workflow, onChange],
|
||||
);
|
||||
|
||||
@@ -13,9 +13,11 @@ import type {
|
||||
WorkflowEdge,
|
||||
WorkflowNode,
|
||||
WorkflowNodeConfig,
|
||||
WorkflowOrchestrationMode,
|
||||
WorkflowValidationIssue,
|
||||
AgentNodeConfig,
|
||||
} from '@shared/domain/workflow';
|
||||
import { isBuilderBasedMode } from '@shared/domain/workflow';
|
||||
import { ModelSelect, ReasoningEffortSelect } from '@renderer/components/AgentConfigFields';
|
||||
import { InvokeFunctionInspector } from '@renderer/components/workflow/InvokeFunctionInspector';
|
||||
import { ConditionEditor } from '@renderer/components/workflow/ConditionEditor';
|
||||
@@ -251,16 +253,19 @@ function PlaceholderNodeInspector({
|
||||
|
||||
function EdgeInspector({
|
||||
edge,
|
||||
orchestrationMode,
|
||||
validationIssues,
|
||||
onEdgeChange,
|
||||
onEdgeRemove,
|
||||
}: {
|
||||
edge: WorkflowEdge;
|
||||
orchestrationMode?: WorkflowOrchestrationMode;
|
||||
validationIssues?: WorkflowValidationIssue[];
|
||||
onEdgeChange: (edgeId: string, patch: Partial<WorkflowEdge>) => void;
|
||||
onEdgeRemove: (edgeId: string) => void;
|
||||
}) {
|
||||
const isFanIn = edge.kind === 'fan-in';
|
||||
const builderMode = isBuilderBasedMode(orchestrationMode);
|
||||
const edgeIssues = validationIssues?.filter((i) => i.edgeId === edge.id) ?? [];
|
||||
|
||||
const handleConditionChange = useCallback(
|
||||
@@ -310,6 +315,7 @@ function EdgeInspector({
|
||||
<input
|
||||
checked={edge.isLoop === true}
|
||||
className="size-4 accent-[var(--color-accent)]"
|
||||
disabled={builderMode}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
onEdgeChange(edge.id, { isLoop: true, maxIterations: edge.maxIterations ?? 10 });
|
||||
@@ -321,14 +327,17 @@ function EdgeInspector({
|
||||
/>
|
||||
</label>
|
||||
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
|
||||
Creates an iterative cycle, re-executing the path between these nodes up to a set limit.
|
||||
{builderMode
|
||||
? 'Loop edges are managed by the orchestration mode settings.'
|
||||
: 'Creates an iterative cycle, re-executing the path between these nodes up to a set limit.'}
|
||||
</p>
|
||||
</div>
|
||||
{edge.isLoop && (
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[11px] text-[var(--color-text-muted)]">Max Iterations</span>
|
||||
<input
|
||||
className="w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-3 py-1.5 text-[12px] text-[var(--color-text-primary)] outline-none transition focus:border-[var(--color-accent)]/50"
|
||||
className="w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-3 py-1.5 text-[12px] text-[var(--color-text-primary)] outline-none transition focus:border-[var(--color-accent)]/50 disabled:opacity-60"
|
||||
disabled={builderMode}
|
||||
min={1}
|
||||
onChange={(e) => {
|
||||
const raw = parseInt(e.target.value, 10);
|
||||
@@ -339,6 +348,11 @@ function EdgeInspector({
|
||||
type="number"
|
||||
value={edge.maxIterations ?? 10}
|
||||
/>
|
||||
{builderMode && (
|
||||
<p className="text-[11px] text-[var(--color-text-muted)]">
|
||||
Derived from the {orchestrationMode === 'group-chat' ? 'Max Rounds' : 'Max Iterations'} setting.
|
||||
</p>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
@@ -414,6 +428,7 @@ export function WorkflowGraphInspector({
|
||||
<div className="p-4">
|
||||
<EdgeInspector
|
||||
edge={selectedEdge}
|
||||
orchestrationMode={workflow.settings.orchestrationMode}
|
||||
onEdgeChange={onEdgeChange}
|
||||
onEdgeRemove={onEdgeRemove}
|
||||
validationIssues={validationIssues}
|
||||
|
||||
Reference in New Issue
Block a user