From d953b002569742b663591b8ab95b21b087339ac7 Mon Sep 17 00:00:00 2001 From: Caleb Martin Date: Tue, 23 Jun 2026 17:04:07 -0700 Subject: [PATCH] feat(apollo-react): add Skills resource type to AgentFlow canvas Adds an opt-in third "+" add-button on the top edge of the agent node (alongside Memory and Escalations) for a new "skills" resource type. - New ResourceNodeType.Skills (top edge) with position/order entries - AgentFlowSkillsResource + SkillsResourceData added to the resource and node-data unions; AgentFlowResourceType now includes 'skills' - New enableSkills prop (mirrors enableMemory) gates the button so it is opt-in per consumer; off by default - skills label added to AgentNodeTranslations (+ default "Skills") - Clicking the button fires onAddResource('skills'); no node rendering paths are required since consumers drive the resulting UI - Storybook wrapper wired with enableSkills for visual testing Co-Authored-By: Claude Opus 4.8 --- .../AgentCanvas/AgentFlow.constants.ts | 13 ++++--- .../AgentCanvas/AgentFlow.stories.tsx | 3 ++ .../components/AgentCanvas/AgentFlow.tsx | 22 ++++++++++- .../AgentCanvas/nodes/AgentNode.tsx | 30 ++++++++++++++- packages/apollo-react/src/canvas/types.ts | 37 ++++++++++++++++++- .../src/canvas/utils/auto-layout.ts | 2 + 6 files changed, 97 insertions(+), 10 deletions(-) diff --git a/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.constants.ts b/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.constants.ts index 861b5d110..f26304a28 100644 --- a/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.constants.ts +++ b/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.constants.ts @@ -7,6 +7,7 @@ export enum ResourceNodeType { Tool = 'tool', MemorySpace = 'memorySpace', A2A = 'a2a', + Skills = 'skills', } export const ResourceNodeTypeToPosition: Record = { @@ -16,16 +17,18 @@ export const ResourceNodeTypeToPosition: Record = { [ResourceNodeType.Tool]: Position.Bottom, [ResourceNodeType.MemorySpace]: Position.Top, [ResourceNodeType.A2A]: Position.Bottom, + [ResourceNodeType.Skills]: Position.Top, }; // Consistent ordering for resource node types -// Top: MemorySpace -> Escalation +// Top: MemorySpace -> Escalation -> Skills // Bottom: Context -> Tool -> MCP -> A2A export const ResourceNodeTypeOrder: Record = { [ResourceNodeType.MemorySpace]: 0, [ResourceNodeType.Escalation]: 1, - [ResourceNodeType.Context]: 2, - [ResourceNodeType.Tool]: 3, - [ResourceNodeType.MCP]: 4, - [ResourceNodeType.A2A]: 5, + [ResourceNodeType.Skills]: 2, + [ResourceNodeType.Context]: 3, + [ResourceNodeType.Tool]: 4, + [ResourceNodeType.MCP]: 5, + [ResourceNodeType.A2A]: 6, }; diff --git a/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.stories.tsx b/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.stories.tsx index e02574649..d78bb3971 100644 --- a/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.stories.tsx +++ b/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.stories.tsx @@ -352,6 +352,7 @@ interface AgentFlowWrapperProps { definition?: any; enableTimelinePlayer?: boolean; enableMemory?: boolean; + enableSkills?: boolean; enableStickyNotes?: boolean; enableInstructions?: boolean; healthScore?: number; @@ -371,6 +372,7 @@ const AgentFlowWrapper = ({ definition = sampleAgentDefinition, enableTimelinePlayer = true, enableMemory = true, + enableSkills = true, enableStickyNotes = true, enableInstructions = false, healthScore, @@ -624,6 +626,7 @@ const AgentFlowWrapper = ({ onSelectResource={handleSelectResource} enableTimelinePlayer={mode === 'view' && enableTimelinePlayer} enableMemory={enableMemory} + enableSkills={enableSkills} enableStickyNotes={enableStickyNotes} enableInstructions={enableInstructions} stickyNotes={stickyNotes} diff --git a/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.tsx b/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.tsx index 9dd9d9fbb..65935b3ed 100644 --- a/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.tsx +++ b/packages/apollo-react/src/canvas/components/AgentCanvas/AgentFlow.tsx @@ -86,11 +86,14 @@ const AGENT_FLOW_FIT_VIEW_OPTIONS = { // agent node wrapper const createAgentNodeWrapper = (handlers: { - onAddResource?: (type: 'context' | 'escalation' | 'mcp' | 'tool' | 'memorySpace' | 'a2a') => void; + onAddResource?: ( + type: 'context' | 'escalation' | 'mcp' | 'tool' | 'memorySpace' | 'a2a' | 'skills' + ) => void; translations?: AgentNodeTranslations; suggestionTranslations?: SuggestionTranslations; enableMcpTools?: boolean; enableMemory?: boolean; + enableSkills?: boolean; enableA2a?: boolean; enableInstructions?: boolean; healthScore?: number; @@ -146,6 +149,15 @@ const createAgentNodeWrapper = (handlers: { node.data.parentNodeId === props.id ); + const hasSkills = + handlers.enableSkills === true && + nodes.some( + (node) => + isAgentFlowResourceNode(node) && + node.data.type === 'skills' && + node.data.parentNodeId === props.id + ); + // Check if agent itself is running OR if any of its resources are running on view mode OR if it's processing a suggestion const agentRunning = hasAgentRunning(storeProps.spans); const resourceRunning = nodes.some( @@ -187,6 +199,7 @@ const createAgentNodeWrapper = (handlers: { hasMcp={hasMcp} hasMemory={hasMemory} hasA2a={hasA2a} + hasSkills={hasSkills} a2aEnabled={handlers.enableA2a === true} mcpEnabled={handlers.enableMcpTools !== false} mode={storeProps.mode} @@ -198,6 +211,7 @@ const createAgentNodeWrapper = (handlers: { translations={handlers.translations ?? DefaultAgentNodeTranslations} suggestionTranslations={handlers.suggestionTranslations ?? DefaultSuggestionTranslations} enableMemory={handlers.enableMemory === true} + enableSkills={handlers.enableSkills === true} enableInstructions={handlers.enableInstructions === true} healthScore={handlers.healthScore} onHealthScoreClick={handlers.onHealthScoreClick} @@ -276,6 +290,7 @@ const AgentFlowInner = memo( canvasRef, enableMcpTools, enableMemory, + enableSkills, enableA2a, enableStickyNotes, enableInstructions, @@ -373,7 +388,7 @@ const AgentFlowInner = memo( const nodeTypes = useMemo(() => { const handleAddResource = ( - type: 'context' | 'escalation' | 'mcp' | 'tool' | 'memorySpace' | 'a2a' + type: 'context' | 'escalation' | 'mcp' | 'tool' | 'memorySpace' | 'a2a' | 'skills' ) => { // Use createResourcePlaceholder which will either create a placeholder or call onAddResource createResourcePlaceholder(type); @@ -389,6 +404,7 @@ const AgentFlowInner = memo( suggestionTranslations, enableMcpTools, enableMemory, + enableSkills, enableA2a, enableInstructions, healthScore, @@ -428,6 +444,7 @@ const AgentFlowInner = memo( onCollapseResource, enableMcpTools, enableMemory, + enableSkills, suggestionGroup?.metadata?.version, ]); @@ -596,6 +613,7 @@ const AgentFlowInner = memo( if (node.data.isVirtual) return true; // Always keep virtual nodes if (node.data.type === 'memorySpace') return !!enableMemory; if (node.data.type === 'a2a') return !!enableA2a; + if (node.data.type === 'skills') return !!enableSkills; return true; }); diff --git a/packages/apollo-react/src/canvas/components/AgentCanvas/nodes/AgentNode.tsx b/packages/apollo-react/src/canvas/components/AgentCanvas/nodes/AgentNode.tsx index 453b2d120..2e49c308d 100644 --- a/packages/apollo-react/src/canvas/components/AgentCanvas/nodes/AgentNode.tsx +++ b/packages/apollo-react/src/canvas/components/AgentCanvas/nodes/AgentNode.tsx @@ -67,15 +67,19 @@ interface AgentNodeProps { hasTool?: boolean; hasMcp?: boolean; hasA2a?: boolean; + hasSkills?: boolean; a2aEnabled?: boolean; mcpEnabled?: boolean; hasError?: boolean; hasSuccess?: boolean; hasRunning?: boolean; - onAddResource?: (type: 'context' | 'escalation' | 'mcp' | 'tool' | 'memorySpace' | 'a2a') => void; + onAddResource?: ( + type: 'context' | 'escalation' | 'mcp' | 'tool' | 'memorySpace' | 'a2a' | 'skills' + ) => void; onAddInstructions?: () => void; translations: AgentNodeTranslations; enableMemory?: boolean; + enableSkills?: boolean; enableInstructions?: boolean; healthScore?: number; onHealthScoreClick?: () => void; @@ -100,6 +104,7 @@ const AgentNodeComponent = memo((props: NodeProps> & AgentNo hasTool = false, hasMcp = false, hasA2a = false, + hasSkills = false, a2aEnabled = false, mcpEnabled = true, hasError = false, @@ -109,6 +114,7 @@ const AgentNodeComponent = memo((props: NodeProps> & AgentNo onAddInstructions, translations, enableMemory, + enableSkills, enableInstructions = false, healthScore, onHealthScoreClick, @@ -209,6 +215,8 @@ const AgentNodeComponent = memo((props: NodeProps> & AgentNo const displayMemory = enableMemory === true && (mode === 'design' || (mode === 'view' && hasMemory)); + const displaySkills = + enableSkills === true && (mode === 'design' || (mode === 'view' && hasSkills)); const displayContext = mode === 'design' || (mode === 'view' && hasContext); const displayEscalation = mode === 'design' || (mode === 'view' && hasEscalation); const displayTool = mode === 'design' || (mode === 'view' && hasTool); @@ -256,6 +264,21 @@ const AgentNodeComponent = memo((props: NodeProps> & AgentNo }); } + if (displaySkills) { + topHandles.push({ + id: ResourceNodeType.Skills, + type: 'source', + handleType: 'artifact', + label: translations.skills, + showButton: mode === 'design', + labelBackgroundColor: 'var(--canvas-background-secondary)', + visible: displaySkills, + onAction: (_e: HandleActionEvent) => { + onAddResource?.('skills'); + }, + }); + } + if (topHandles.length) { configs.push({ position: Position.Top, @@ -310,6 +333,7 @@ const AgentNodeComponent = memo((props: NodeProps> & AgentNo mode, displayContext, displayMemory, + displaySkills, displayMcp, displayTool, displayA2a, @@ -603,6 +627,7 @@ const AgentNodeWrapper = (props: NodeProps> & AgentNodeProps hasTool, hasMcp, hasA2a, + hasSkills, a2aEnabled, mcpEnabled, hasError, @@ -612,6 +637,7 @@ const AgentNodeWrapper = (props: NodeProps> & AgentNodeProps onAddInstructions, translations, enableMemory, + enableSkills, enableInstructions, healthScore, onHealthScoreClick, @@ -631,6 +657,7 @@ const AgentNodeWrapper = (props: NodeProps> & AgentNodeProps hasTool={hasTool} hasMcp={hasMcp} hasA2a={hasA2a} + hasSkills={hasSkills} a2aEnabled={a2aEnabled} mcpEnabled={mcpEnabled} hasError={hasError} @@ -640,6 +667,7 @@ const AgentNodeWrapper = (props: NodeProps> & AgentNodeProps onAddInstructions={onAddInstructions} translations={translations} enableMemory={enableMemory} + enableSkills={enableSkills} enableInstructions={enableInstructions} healthScore={healthScore} onHealthScoreClick={onHealthScoreClick} diff --git a/packages/apollo-react/src/canvas/types.ts b/packages/apollo-react/src/canvas/types.ts index 90a5f833c..cc6414f48 100644 --- a/packages/apollo-react/src/canvas/types.ts +++ b/packages/apollo-react/src/canvas/types.ts @@ -161,13 +161,29 @@ export type AgentFlowA2aResource = { errorAction?: Omit; }; +export type AgentFlowSkillsResource = { + id: string; + type: 'skills'; + name: string; + originalName?: string; + description: string; + errors?: ErrorInfo[]; + hasBreakpoint?: boolean; + isCurrentBreakpoint?: boolean; + hasGuardrails?: boolean; + projectId?: string; + isDisabled?: boolean; + errorAction?: Omit; +}; + export type AgentFlowResource = | AgentFlowContextResource | AgentFlowEscalationResource | AgentFlowMcpResource | AgentFlowToolResource | AgentFlowMemorySpaceResource - | AgentFlowA2aResource; + | AgentFlowA2aResource + | AgentFlowSkillsResource; export type AgentFlowResourceType = AgentFlowResource['type']; /** @@ -276,7 +292,16 @@ export type AgentFlowProps = { resources: AgentFlowResource[]; allowDragging?: boolean; initialSelectedResource?: { - type: 'context' | 'escalation' | 'mcp' | 'pane' | 'run' | 'tool' | 'memorySpace' | 'a2a'; + type: + | 'context' + | 'escalation' + | 'mcp' + | 'pane' + | 'run' + | 'tool' + | 'memorySpace' + | 'a2a' + | 'skills'; name: string; } | null; onSelectResource?: (resourceId: string | null) => void; @@ -354,6 +379,8 @@ export type AgentFlowProps = { enableMcpTools?: boolean; /** TODO: Remove once memory feature is fully implemented */ enableMemory?: boolean; + /** Enables the "Skills" add-button on the top edge of the agent node. */ + enableSkills?: boolean; enableA2a?: boolean; enableStickyNotes?: boolean; enableInstructions?: boolean; @@ -465,6 +492,9 @@ export type MemorySpaceResourceData = { export type A2aResourceData = { type: 'a2a'; }; +export type SkillsResourceData = { + type: 'skills'; +}; export type SharedResourceData = { name: string; @@ -512,6 +542,7 @@ export type AgentFlowResourceNodeData = ( | ToolResourceData | MemorySpaceResourceData | A2aResourceData + | SkillsResourceData ) & SharedResourceData; export type AgentFlowResourceNode = Node & { @@ -552,6 +583,7 @@ export interface AgentNodeTranslations { context: string; tools: string; memory: string; + skills: string; instructions: string; addInstructions: string; // Settings preview @@ -579,6 +611,7 @@ export const DefaultAgentNodeTranslations: AgentNodeTranslations = { context: 'Context', tools: 'Tools', memory: 'Memory', + skills: 'Skills', instructions: 'Instructions', addInstructions: 'Add Instructions', // Settings preview diff --git a/packages/apollo-react/src/canvas/utils/auto-layout.ts b/packages/apollo-react/src/canvas/utils/auto-layout.ts index ded3e3ceb..f59b5efac 100644 --- a/packages/apollo-react/src/canvas/utils/auto-layout.ts +++ b/packages/apollo-react/src/canvas/utils/auto-layout.ts @@ -40,6 +40,7 @@ export const getAgentGroupBottomPosition = ( [ResourceNodeType.Tool]: [], [ResourceNodeType.MemorySpace]: [], [ResourceNodeType.A2A]: [], + [ResourceNodeType.Skills]: [], }; // Group nodes by which handle they're connected to @@ -94,6 +95,7 @@ const arrangeAgent = ( [ResourceNodeType.Tool]: [], [ResourceNodeType.MemorySpace]: [], [ResourceNodeType.A2A]: [], + [ResourceNodeType.Skills]: [], }; // Group nodes by which handle they're connected to, excluding nodes with explicit positions