mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-29 14:07:13 +02:00
fix: edge selection highlighting and group-chat connection rules
- Add CSS styling for selected edges (indigo highlight with glow) - Enable orchestrator↔agent connections in group-chat mode - Downgrade disconnected-agent validation from error to warning - Rename addHandoffEdge to addEdge (generic for all modes) - Add tests for group-chat connection rules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -22,7 +22,7 @@ import type { OrchestrationMode, PatternDefinition, PatternGraph } from '@shared
|
|||||||
import { resolvePatternGraph } from '@shared/domain/pattern';
|
import { resolvePatternGraph } from '@shared/domain/pattern';
|
||||||
import type { ModelDefinition } from '@shared/domain/models';
|
import type { ModelDefinition } from '@shared/domain/models';
|
||||||
import {
|
import {
|
||||||
addHandoffEdge,
|
addEdge,
|
||||||
autoLayoutGraph,
|
autoLayoutGraph,
|
||||||
fromCanvasPositions,
|
fromCanvasPositions,
|
||||||
isConnectionAllowed,
|
isConnectionAllowed,
|
||||||
@@ -143,7 +143,7 @@ function PatternGraphCanvasInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (connection.source && connection.target) {
|
if (connection.source && connection.target) {
|
||||||
const updatedGraph = addHandoffEdge(graph, connection.source, connection.target);
|
const updatedGraph = addEdge(graph, connection.source, connection.target);
|
||||||
onGraphChange(updatedGraph);
|
onGraphChange(updatedGraph);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -208,8 +208,10 @@ export function isConnectionAllowed(
|
|||||||
return false;
|
return false;
|
||||||
case 'handoff':
|
case 'handoff':
|
||||||
return sourceNode.kind === 'agent' && targetNode.kind === 'agent';
|
return sourceNode.kind === 'agent' && targetNode.kind === 'agent';
|
||||||
case 'group-chat':
|
case 'group-chat': {
|
||||||
return false;
|
const kinds = new Set([sourceNode.kind, targetNode.kind]);
|
||||||
|
return kinds.has('orchestrator') && kinds.has('agent');
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -226,7 +228,7 @@ function edgeId(source: string, target: string): string {
|
|||||||
return `edge-${source}-to-${target}`;
|
return `edge-${source}-to-${target}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addHandoffEdge(graph: PatternGraph, source: string, target: string): PatternGraph {
|
export function addEdge(graph: PatternGraph, source: string, target: string): PatternGraph {
|
||||||
const newEdge: PatternGraphEdge = {
|
const newEdge: PatternGraphEdge = {
|
||||||
id: edgeId(source, target),
|
id: edgeId(source, target),
|
||||||
source,
|
source,
|
||||||
|
|||||||
@@ -150,6 +150,12 @@ textarea {
|
|||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-flow__edge.selected .react-flow__edge-path {
|
||||||
|
stroke: #818cf8;
|
||||||
|
stroke-width: 2.5;
|
||||||
|
filter: drop-shadow(0 0 4px rgba(129, 140, 248, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
.react-flow__edge.animated .react-flow__edge-path {
|
.react-flow__edge.animated .react-flow__edge-path {
|
||||||
stroke-dasharray: 5;
|
stroke-dasharray: 5;
|
||||||
animation: reactflow-dash 0.5s linear infinite;
|
animation: reactflow-dash 0.5s linear infinite;
|
||||||
|
|||||||
@@ -531,8 +531,13 @@ function getAgentNodes(graph: PatternGraph): PatternGraphNode[] {
|
|||||||
return graph.nodes.filter((node) => node.kind === 'agent');
|
return graph.nodes.filter((node) => node.kind === 'agent');
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushGraphIssue(issues: PatternValidationIssue[], message: string, field = 'graph'): void {
|
function pushGraphIssue(
|
||||||
issues.push({ level: 'error', field, message });
|
issues: PatternValidationIssue[],
|
||||||
|
message: string,
|
||||||
|
field = 'graph',
|
||||||
|
level: 'error' | 'warning' = 'error',
|
||||||
|
): void {
|
||||||
|
issues.push({ level, field, message });
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAdjacency(graph: PatternGraph): {
|
function buildAdjacency(graph: PatternGraph): {
|
||||||
@@ -779,7 +784,7 @@ function validateHandoffGraph(
|
|||||||
|
|
||||||
for (const agentNode of agentNodes) {
|
for (const agentNode of agentNodes) {
|
||||||
if (!reachable.has(agentNode.id)) {
|
if (!reachable.has(agentNode.id)) {
|
||||||
pushGraphIssue(issues, `Handoff entry agent must be able to reach "${agentNode.agentId}".`);
|
pushGraphIssue(issues, `Handoff entry agent must be able to reach "${agentNode.agentId}".`, 'graph', 'warning');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,7 +812,7 @@ function validateGroupChatGraph(
|
|||||||
const orchestratorSources = new Set((incoming.get(orchestratorNode.id) ?? []).map((edge) => edge.source));
|
const orchestratorSources = new Set((incoming.get(orchestratorNode.id) ?? []).map((edge) => edge.source));
|
||||||
|
|
||||||
if (graph.edges.length !== pattern.agents.length * 2 + 2) {
|
if (graph.edges.length !== pattern.agents.length * 2 + 2) {
|
||||||
pushGraphIssue(issues, 'Group chat graphs must connect the orchestrator to every participant and then back to user output.');
|
pushGraphIssue(issues, 'Group chat graphs must connect the orchestrator to every participant and then back to user output.', 'graph', 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((outgoing.get(inputNode.id) ?? []).some((edge) => edge.target !== orchestratorNode.id)) {
|
if ((outgoing.get(inputNode.id) ?? []).some((edge) => edge.target !== orchestratorNode.id)) {
|
||||||
@@ -820,11 +825,11 @@ function validateGroupChatGraph(
|
|||||||
|
|
||||||
for (const agentNode of agentNodes) {
|
for (const agentNode of agentNodes) {
|
||||||
if (!orchestratorTargets.has(agentNode.id)) {
|
if (!orchestratorTargets.has(agentNode.id)) {
|
||||||
pushGraphIssue(issues, `Orchestrator must connect to agent "${agentNode.agentId}".`);
|
pushGraphIssue(issues, `Orchestrator must connect to agent "${agentNode.agentId}".`, 'graph', 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!orchestratorSources.has(agentNode.id)) {
|
if (!orchestratorSources.has(agentNode.id)) {
|
||||||
pushGraphIssue(issues, `Agent "${agentNode.agentId}" must connect back to the orchestrator.`);
|
pushGraphIssue(issues, `Agent "${agentNode.agentId}" must connect back to the orchestrator.`, 'graph', 'warning');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { describe, expect, test } from 'bun:test';
|
|||||||
import { createBuiltinPatterns, resolvePatternGraph, type PatternDefinition } from '@shared/domain/pattern';
|
import { createBuiltinPatterns, resolvePatternGraph, type PatternDefinition } from '@shared/domain/pattern';
|
||||||
import {
|
import {
|
||||||
addAgentNodeToGraph,
|
addAgentNodeToGraph,
|
||||||
addHandoffEdge,
|
addEdge,
|
||||||
autoLayoutGraph,
|
autoLayoutGraph,
|
||||||
canMoveSequential,
|
canMoveSequential,
|
||||||
findAgentForNode,
|
findAgentForNode,
|
||||||
@@ -148,19 +148,53 @@ describe('pattern graph connection rules', () => {
|
|||||||
);
|
);
|
||||||
expect(allowed).toBe(false);
|
expect(allowed).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('group-chat mode allows orchestrator-to-agent connections', () => {
|
||||||
|
const pattern = findPattern('group-chat');
|
||||||
|
const graph = resolvePatternGraph(pattern);
|
||||||
|
const orchestratorNode = graph.nodes.find((n) => n.kind === 'orchestrator')!;
|
||||||
|
const agentNode = graph.nodes.find((n) => n.kind === 'agent')!;
|
||||||
|
|
||||||
|
const orcToAgent = isConnectionAllowed(
|
||||||
|
{ source: orchestratorNode.id, target: agentNode.id, sourceHandle: null, targetHandle: null },
|
||||||
|
'group-chat',
|
||||||
|
graph,
|
||||||
|
);
|
||||||
|
expect(orcToAgent).toBe(true);
|
||||||
|
|
||||||
|
const agentToOrc = isConnectionAllowed(
|
||||||
|
{ source: agentNode.id, target: orchestratorNode.id, sourceHandle: null, targetHandle: null },
|
||||||
|
'group-chat',
|
||||||
|
graph,
|
||||||
|
);
|
||||||
|
expect(agentToOrc).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('group-chat mode disallows agent-to-agent connections', () => {
|
||||||
|
const pattern = findPattern('group-chat');
|
||||||
|
const graph = resolvePatternGraph(pattern);
|
||||||
|
const agentNodes = graph.nodes.filter((n) => n.kind === 'agent');
|
||||||
|
|
||||||
|
const allowed = isConnectionAllowed(
|
||||||
|
{ source: agentNodes[0]!.id, target: agentNodes[1]!.id, sourceHandle: null, targetHandle: null },
|
||||||
|
'group-chat',
|
||||||
|
graph,
|
||||||
|
);
|
||||||
|
expect(allowed).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('pattern graph mutation helpers', () => {
|
describe('pattern graph mutation helpers', () => {
|
||||||
test('addHandoffEdge adds a new edge between agent nodes', () => {
|
test('addEdge adds a new edge between agent nodes', () => {
|
||||||
const pattern = findPattern('handoff');
|
const pattern = findPattern('handoff');
|
||||||
const graph = resolvePatternGraph(pattern);
|
const graph = resolvePatternGraph(pattern);
|
||||||
const agentNodes = graph.nodes.filter((n) => n.kind === 'agent');
|
const agentNodes = graph.nodes.filter((n) => n.kind === 'agent');
|
||||||
const initialEdgeCount = graph.edges.length;
|
const initialEdgeCount = graph.edges.length;
|
||||||
|
|
||||||
const updated = addHandoffEdge(graph, agentNodes[1]!.id, agentNodes[2]!.id);
|
const updated = addEdge(graph, agentNodes[1]!.id, agentNodes[2]!.id);
|
||||||
expect(updated.edges.length).toBe(initialEdgeCount + 1);
|
expect(updated.edges.length).toBe(initialEdgeCount + 1);
|
||||||
|
|
||||||
const duplicated = addHandoffEdge(updated, agentNodes[1]!.id, agentNodes[2]!.id);
|
const duplicated = addEdge(updated, agentNodes[1]!.id, agentNodes[2]!.id);
|
||||||
expect(duplicated.edges.length).toBe(initialEdgeCount + 1);
|
expect(duplicated.edges.length).toBe(initialEdgeCount + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user