import type { CredentialRequirement, WorkflowDefinition } from "@codemation/core"; import { AgentConfigInspector, AgentConnectionNodeCollector, type AgentConnectionNodeDescriptor, ConnectionNodeIdFactory, inject, injectable, } from "@codemation/core"; import { McpServerCatalog } from "../../mcp/McpServerCatalog"; export type WorkflowCredentialSlotRef = Readonly<{ workflowId: string; nodeId: string; nodeName: string; requirement: CredentialRequirement; }>; @injectable() export class WorkflowCredentialNodeResolver { constructor( @inject(McpServerCatalog) private readonly mcpCatalog?: McpServerCatalog, ) {} describeCredentialNodeDisplay(workflow: WorkflowDefinition, nodeId: string): string { const direct = workflow.nodes.find((n) => n.id === nodeId); if (direct) { return direct.name ?? direct.config.name ?? direct.id; } const recursive = this.findRecursiveConnectionNode(workflow, nodeId); if (!recursive) { return nodeId; } return this.buildRecursiveDisplayLabel(recursive.rootAgentLabel, recursive.entry, recursive.entriesById); } isCredentialNodeIdInWorkflow(workflow: WorkflowDefinition, nodeId: string): boolean { if (workflow.nodes.some((n) => n.id === nodeId)) { return true; } return this.findRecursiveConnectionNode(workflow, nodeId) !== undefined; } findRequirement( workflow: WorkflowDefinition, nodeId: string, slotKey: string, ): Readonly<{ nodeName: string; requirement: CredentialRequirement }> | undefined { const direct = this.findDirectRequirement(workflow, nodeId, slotKey); if (direct) { return direct; } const recursive = this.findRecursiveConnectionNode(workflow, nodeId); if (!recursive) { return undefined; } const requirement = recursive.entry.credentialSource .getCredentialRequirements?.() ?.find((entry) => entry.slotKey === slotKey); return requirement ? { nodeName: recursive.entry.name, requirement } : undefined; } listSlots(workflow: WorkflowDefinition): ReadonlyArray { const slotsByKey = new Map(); for (const node of workflow.nodes) { if (AgentConfigInspector.isAgentNodeConfig(node.config)) { this.addRecursiveAgentSlots(workflow.id, node.id, node.config, slotsByKey); continue; } this.addSlotsForRequirements( workflow.id, node.id, node.name ?? node.config.name ?? node.id, node.config.getCredentialRequirements?.() ?? [], slotsByKey, ); } return [...slotsByKey.values()]; } private findDirectRequirement( workflow: WorkflowDefinition, nodeId: string, slotKey: string, ): Readonly<{ nodeName: string; requirement: CredentialRequirement }> | undefined { const node = workflow.nodes.find((entry) => entry.id === nodeId); if (!node || AgentConfigInspector.isAgentNodeConfig(node.config)) { return undefined; } const requirement = node.config.getCredentialRequirements?.()?.find((entry) => entry.slotKey === slotKey); if (!requirement) { return undefined; } return { nodeName: node.name ?? node.config.name ?? node.id, requirement }; } private addRecursiveAgentSlots( workflowId: string, rootAgentNodeId: string, agentConfig: Parameters[1], slotsByKey: Map, ): void { const mcpResolver = this.mcpCatalog ? (id: string) => this.mcpCatalog!.get(id) : undefined; const descriptors = AgentConnectionNodeCollector.collect(rootAgentNodeId, agentConfig, mcpResolver); for (const entry of descriptors) { this.addSlotsForRequirements( workflowId, entry.nodeId, entry.name, entry.credentialSource.getCredentialRequirements?.() ?? [], slotsByKey, ); } } private addSlotsForRequirements( workflowId: string, nodeId: string, nodeName: string, requirements: ReadonlyArray, slotsByKey: Map, ): void { for (const requirement of requirements) { const key = `${nodeId}\0${requirement.slotKey}`; if (slotsByKey.has(key)) { continue; } slotsByKey.set(key, { workflowId, nodeId, nodeName, requirement, }); } } private findRecursiveConnectionNode( workflow: WorkflowDefinition, nodeId: string, ): | Readonly<{ rootAgentNodeId: string; rootAgentLabel: string; entry: AgentConnectionNodeDescriptor; entriesById: ReadonlyMap; }> | undefined { if ( !ConnectionNodeIdFactory.isLanguageModelConnectionNodeId(nodeId) && !ConnectionNodeIdFactory.isToolConnectionNodeId(nodeId) && !ConnectionNodeIdFactory.isMcpConnectionNodeId(nodeId) ) { return undefined; } const mcpResolver = this.mcpCatalog ? (id: string) => this.mcpCatalog!.get(id) : undefined; for (const node of workflow.nodes) { if (!AgentConfigInspector.isAgentNodeConfig(node.config)) { continue; } const entries = AgentConnectionNodeCollector.collect(node.id, node.config, mcpResolver); const entriesById = new Map(entries.map((entry) => [entry.nodeId, entry])); const entry = entriesById.get(nodeId); if (!entry) { continue; } return { rootAgentNodeId: node.id, rootAgentLabel: node.name ?? node.config.name ?? node.id, entry, entriesById, }; } return undefined; } private buildRecursiveDisplayLabel( rootAgentLabel: string, entry: AgentConnectionNodeDescriptor, entriesById: ReadonlyMap, ): string { const labels = [rootAgentLabel, ...this.collectAncestorToolLabels(entry.parentNodeId, entriesById)]; labels.push(entry.role === "languageModel" ? "Language model" : entry.name); return labels.join(" › "); } private collectAncestorToolLabels( parentNodeId: string, entriesById: ReadonlyMap, ): ReadonlyArray { const labels: string[] = []; let currentNodeId = parentNodeId; while (true) { const parentEntry = entriesById.get(currentNodeId); if (!parentEntry) { return labels.reverse(); } if (parentEntry.role === "tool" || parentEntry.role === "nestedAgent") { labels.push(parentEntry.name); } currentNodeId = parentEntry.parentNodeId; } } }