n8n
Version:
n8n Workflow Automation Tool
301 lines • 16.5 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentConfigService = void 0;
const agent_config_1 = require("@n8n/ai-utilities/agent-config");
const api_types_1 = require("@n8n/api-types");
const backend_common_1 = require("@n8n/backend-common");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const n8n_workflow_1 = require("n8n-workflow");
const credentials_service_1 = require("../../credentials/credentials.service");
const not_found_error_1 = require("../../errors/response-errors/not-found.error");
const event_service_1 = require("../../events/event.service");
const agent_modification_telemetry_service_1 = require("./agent-modification-telemetry.service");
const agent_runtime_cache_service_1 = require("./agent-runtime-cache.service");
const agent_setup_completion_service_1 = require("./agent-setup-completion.service");
const agent_skills_service_1 = require("./agent-skills.service");
const integrations_sync_1 = require("./integrations/integrations-sync");
const agent_config_composition_1 = require("./json-config/agent-config-composition");
const node_tool_ai_gateway_service_1 = require("./json-config/node-tool-ai-gateway.service");
const sanitize_unknown_agent_credentials_1 = require("./json-config/sanitize-unknown-agent-credentials");
const agent_task_repository_1 = require("./repositories/agent-task.repository");
const agent_repository_1 = require("./repositories/agent.repository");
const workflow_tool_workflow_resolver_1 = require("./tools/workflow-tool-workflow-resolver");
const agent_credential_provider_1 = require("./utils/agent-credential-provider");
const agent_draft_utils_1 = require("./utils/agent-draft.utils");
const node_tool_validation_1 = require("./utils/node-tool-validation");
const sub_agent_resolver_1 = require("./utils/sub-agent-resolver");
let AgentConfigService = class AgentConfigService {
constructor(logger, agentRepository, agentTaskRepository, agentSkillsService, runtimeCacheService, credentialsService, workflowRepository, nodeToolAiGatewayService, eventService, setupCompletionService, modificationTelemetry) {
this.logger = logger;
this.agentRepository = agentRepository;
this.agentTaskRepository = agentTaskRepository;
this.agentSkillsService = agentSkillsService;
this.runtimeCacheService = runtimeCacheService;
this.credentialsService = credentialsService;
this.workflowRepository = workflowRepository;
this.nodeToolAiGatewayService = nodeToolAiGatewayService;
this.eventService = eventService;
this.setupCompletionService = setupCompletionService;
this.modificationTelemetry = modificationTelemetry;
}
async getConfig(agentId, projectId) {
const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!entity)
throw new not_found_error_1.NotFoundError('Agent not found');
const config = (0, agent_config_composition_1.composeJsonConfig)(entity);
if (!config) {
throw new n8n_workflow_1.UserError('Agent has no JSON config yet.');
}
return config;
}
async validateConfig(raw) {
if (hasNodeToolInputSchema(raw)) {
return { valid: false, error: 'Node tool configs must not include inputSchema.' };
}
const parsed = api_types_1.AgentJsonConfigSchema.safeParse((0, api_types_1.sanitizeAgentJsonConfig)(raw));
if (!parsed.success) {
return { valid: false, error: (0, api_types_1.formatAgentConfigZodError)(parsed.error) };
}
const config = parsed.data;
const toolNameCollisions = (0, api_types_1.findVectorStoreToolNameCollisions)(config);
if (toolNameCollisions.length > 0) {
return {
valid: false,
error: `Vector store tool name collides with an existing tool: ${toolNameCollisions.join(', ')}`,
};
}
try {
(0, node_tool_validation_1.validateNodeToolExpressions)(config.tools);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
valid: false,
error: `Invalid $fromAI expression in node tool config: ${message}`,
};
}
const nodeError = await (0, node_tool_validation_1.validateNodeToolConfigs)(config.tools);
if (nodeError) {
return { valid: false, error: nodeError };
}
return { valid: true, config };
}
async updateConfig(agentId, projectId, config, user, options) {
const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!entity)
throw new not_found_error_1.NotFoundError('Agent not found');
const credentialProvider = (0, agent_credential_provider_1.createAgentCredentialProvider)(this.credentialsService, projectId, user);
const accessibleCredentials = await credentialProvider.list();
const accessibleCredentialIds = new Set(accessibleCredentials.map((credential) => credential.id));
const sanitizedBaseConfig = (0, api_types_1.sanitizeAgentJsonConfig)(config);
const sanitizedConfig = (0, sanitize_unknown_agent_credentials_1.sanitizeUnknownAgentCredentials)(sanitizedBaseConfig, accessibleCredentialIds);
const result = await this.validateConfig(sanitizedConfig);
if (!result.valid) {
throw new n8n_workflow_1.UserError(`Invalid agent config: ${result.error}`);
}
const validatedConfig = (0, agent_config_1.reconcileNativeWebSearch)(result.config);
if (validatedConfig.tools !== undefined) {
await this.nodeToolAiGatewayService.assignManagedCredentials(validatedConfig.tools, new Set(accessibleCredentials.map((credential) => credential.type)));
await (0, workflow_tool_workflow_resolver_1.normalizeWorkflowToolRefs)(this.workflowRepository, validatedConfig.tools, projectId);
}
const tasksProvided = validatedConfig.tasks !== undefined;
const existingTaskIds = tasksProvided
? (await this.agentTaskRepository.findByAgentId(agentId)).map((task) => task.id)
: [];
const resolvedSubAgents = await this.removeMissingConfigRefs(validatedConfig, entity, new Set(existingTaskIds));
this.validateSubAgentRefs(resolvedSubAgents, entity);
const previousIntegrations = entity.integrations ?? [];
const previousSchema = entity.schema ?? null;
const integrationsProvided = validatedConfig.integrations !== undefined;
const toolsProvided = validatedConfig.tools !== undefined;
const skillsProvided = validatedConfig.skills !== undefined;
const credentialProvided = validatedConfig.credential !== undefined;
const personalisationProvided = validatedConfig.personalisation !== undefined;
const memoryProvided = validatedConfig.memory !== undefined;
const subAgentsProvided = validatedConfig.subAgents !== undefined;
const providerToolsProvided = validatedConfig.providerTools !== undefined;
const configBlockProvided = validatedConfig.config !== undefined;
const mcpServersProvided = validatedConfig.mcpServers !== undefined;
const vectorStoresProvided = validatedConfig.vectorStores !== undefined;
const { schemaConfig: decomposedSchema, integrations: decomposedIntegrations } = (0, agent_config_composition_1.decomposeJsonConfig)(validatedConfig);
const nextIntegrations = integrationsProvided ? decomposedIntegrations : previousIntegrations;
const nextPersonalisation = personalisationProvided
? options?.clearOmittedOptionalFields
? decomposedSchema.personalisation
: mergePersonalisationWithPreviousGradient(decomposedSchema.personalisation, previousSchema, config)
: undefined;
const nextSchema = {
...omitLegacyAgentDescription(previousSchema),
name: decomposedSchema.name,
model: decomposedSchema.model,
instructions: decomposedSchema.instructions,
...(credentialProvided ? { credential: decomposedSchema.credential } : {}),
...(personalisationProvided ? { personalisation: nextPersonalisation } : {}),
...(memoryProvided ? { memory: decomposedSchema.memory } : {}),
...(subAgentsProvided ? { subAgents: decomposedSchema.subAgents } : {}),
...(toolsProvided ? { tools: decomposedSchema.tools } : {}),
...(skillsProvided ? { skills: decomposedSchema.skills } : {}),
...(tasksProvided ? { tasks: decomposedSchema.tasks } : {}),
...(providerToolsProvided ? { providerTools: decomposedSchema.providerTools } : {}),
...(configBlockProvided ? { config: decomposedSchema.config } : {}),
...(mcpServersProvided ? { mcpServers: decomposedSchema.mcpServers } : {}),
...(vectorStoresProvided ? { vectorStores: decomposedSchema.vectorStores } : {}),
};
if (options?.clearOmittedOptionalFields) {
clearOmittedOptionalFields(nextSchema, validatedConfig);
}
const changedParts = (0, agent_modification_telemetry_service_1.diffAgentConfigParts)(previousSchema, nextSchema, previousIntegrations, nextIntegrations);
entity.schema = nextSchema;
entity.name = validatedConfig.name;
entity.integrations = nextIntegrations;
(0, agent_draft_utils_1.markAgentDraftDirty)(entity);
if (toolsProvided) {
const referencedIds = new Set((validatedConfig.tools ?? [])
.filter((t) => t.type === 'custom')
.map((t) => t.id));
const orphanIds = Object.keys(entity.tools).filter((id) => !referencedIds.has(id));
if (orphanIds.length > 0) {
const tools = { ...entity.tools };
for (const id of orphanIds) {
delete tools[id];
}
entity.tools = tools;
}
}
if (skillsProvided) {
this.agentSkillsService.removeUnreferencedSkills(entity, validatedConfig);
}
this.runtimeCacheService.clearRuntimes(agentId);
const emitSetupCompleted = await this.setupCompletionService.recordIfSetupComplete(entity, projectId, credentialProvider, user);
const saved = await this.agentRepository.save(entity);
this.eventService.emit('agent-saved', { agentId });
this.logger.debug('Updated agent JSON config', { agentId, projectId });
this.modificationTelemetry.record({
agent: saved,
projectId,
user,
by: options.modifiedBy,
changedParts,
wasUnconfigured: (0, agent_modification_telemetry_service_1.isUnconfiguredAgent)(previousSchema, previousIntegrations),
});
await emitSetupCompleted?.();
if (tasksProvided) {
const referencedTaskIds = new Set((validatedConfig.tasks ?? []).map((ref) => ref.id));
const orphanTaskIds = existingTaskIds.filter((id) => !referencedTaskIds.has(id));
if (orphanTaskIds.length > 0) {
await this.agentTaskRepository.delete(orphanTaskIds);
}
}
if (integrationsProvided) {
await (0, integrations_sync_1.syncAgentIntegrations)(saved, previousIntegrations, nextIntegrations, this.logger);
}
return {
config: (0, agent_config_composition_1.composeJsonConfig)(saved) ?? validatedConfig,
updatedAt: saved.updatedAt.toISOString(),
versionId: saved.versionId,
};
}
async removeMissingConfigRefs(config, entity, existingTaskIds) {
if (config.skills !== undefined) {
const skills = entity.skills ?? {};
config.skills = config.skills.filter((ref) => Boolean(skills[ref.id]));
}
if (config.tools !== undefined) {
const tools = entity.tools ?? {};
config.tools = config.tools.filter((ref) => ref.type !== 'custom' || Boolean(tools[ref.id]));
}
if (config.tasks !== undefined) {
config.tasks = config.tasks.filter((ref) => existingTaskIds.has(ref.id));
}
if (config.subAgents?.agents !== undefined) {
const resolvedSubAgents = await (0, sub_agent_resolver_1.resolveUniqueSubAgents)({
refs: config.subAgents.agents,
projectId: entity.projectId,
agentRepository: this.agentRepository,
});
config.subAgents.agents = resolvedSubAgents
.filter(({ agent }) => agent !== null)
.map(({ agentId, useWhen }) => ({
agentId,
...(useWhen ? { useWhen } : {}),
}));
return resolvedSubAgents;
}
return [];
}
validateSubAgentRefs(resolvedSubAgents, entity) {
for (const { agentId, agent } of resolvedSubAgents) {
if (!agent)
continue;
if (agentId === entity.id) {
throw new n8n_workflow_1.UserError('Invalid agent config: An agent cannot use itself as a subagent');
}
if (!agent.activeVersionId) {
throw new n8n_workflow_1.UserError(`Invalid agent config: Subagent "${agentId}" must be published`);
}
}
}
};
exports.AgentConfigService = AgentConfigService;
exports.AgentConfigService = AgentConfigService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger, agent_repository_1.AgentRepository, agent_task_repository_1.AgentTaskRepository, agent_skills_service_1.AgentSkillsService, agent_runtime_cache_service_1.AgentRuntimeCacheService, credentials_service_1.CredentialsService, db_1.WorkflowRepository, node_tool_ai_gateway_service_1.NodeToolAiGatewayService, event_service_1.EventService, agent_setup_completion_service_1.AgentSetupCompletionService, agent_modification_telemetry_service_1.AgentModificationTelemetryService])
], AgentConfigService);
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function mergePersonalisationWithPreviousGradient(personalisation, previousSchema, rawConfig) {
if (!personalisation || !isRecord(rawConfig) || !isRecord(rawConfig.personalisation)) {
return personalisation;
}
if (rawConfig.personalisation.gradient !== undefined)
return personalisation;
const previousGradient = previousSchema?.personalisation?.gradient;
if (!previousGradient)
return personalisation;
return {
...personalisation,
gradient: previousGradient,
};
}
function hasNodeToolInputSchema(raw) {
if (!isRecord(raw) || !Array.isArray(raw.tools))
return false;
return raw.tools.some((tool) => isRecord(tool) && tool.type === 'node' && 'inputSchema' in tool);
}
function clearOmittedOptionalFields(schema, submitted) {
const optionalFields = [
'credential',
'personalisation',
'memory',
'subAgents',
'tools',
'skills',
'tasks',
'providerTools',
'config',
'mcpServers',
'vectorStores',
];
for (const field of optionalFields) {
if (submitted[field] === undefined)
delete schema[field];
}
}
function omitLegacyAgentDescription(config) {
if (!config)
return {};
const { description: _description, ...rest } = config;
return rest;
}
//# sourceMappingURL=agent-config.service.js.map