n8n
Version:
n8n Workflow Automation Tool
1,080 lines • 52.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
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 __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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.AgentsService = void 0;
exports.chatThreadId = chatThreadId;
const api_types_1 = require("@n8n/api-types");
const agents = __importStar(require("@n8n/agents"));
const ai_utilities_1 = require("@n8n/ai-utilities");
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const constants_1 = require("@n8n/constants");
const db_1 = require("@n8n/db");
const decorators_1 = require("@n8n/decorators");
const di_1 = require("@n8n/di");
const typeorm_1 = require("@n8n/typeorm");
const n8n_workflow_1 = require("n8n-workflow");
const uuid_1 = require("uuid");
const active_executions_1 = require("../../active-executions");
const credentials_service_1 = require("../../credentials/credentials.service");
const conflict_error_1 = require("../../errors/response-errors/conflict.error");
const not_found_error_1 = require("../../errors/response-errors/not-found.error");
const node_definition_resolver_1 = require("../../modules/instance-ai/node-definition-resolver");
const node_execution_1 = require("../../node-execution");
const publisher_service_1 = require("../../scaling/pubsub/publisher.service");
const url_service_1 = require("../../services/url.service");
const ttl_map_1 = require("../../utils/ttl-map");
const workflow_runner_1 = require("../../workflow-runner");
const workflow_finder_service_1 = require("../../workflows/workflow-finder.service");
const agents_credential_provider_1 = require("./adapters/agents-credential-provider");
const agent_draft_utils_1 = require("./utils/agent-draft.utils");
const agent_resource_id_1 = require("./utils/agent-resource-id");
const agent_execution_service_1 = require("./agent-execution.service");
const agent_skills_service_1 = require("./agent-skills.service");
const agents_tools_service_1 = require("./agents-tools.service");
const builder_tool_names_1 = require("./builder/builder-tool-names");
const execution_recorder_1 = require("./execution-recorder");
const agent_chat_integration_1 = require("./integrations/agent-chat-integration");
const integrations_sync_1 = require("./integrations/integrations-sync");
const n8n_checkpoint_storage_1 = require("./integrations/n8n-checkpoint-storage");
const n8n_memory_1 = require("./integrations/n8n-memory");
const agent_config_composition_1 = require("./json-config/agent-config-composition");
const agent_json_config_1 = require("./json-config/agent-json-config");
const from_json_config_1 = require("./json-config/from-json-config");
const agent_published_version_repository_1 = require("./repositories/agent-published-version.repository");
const agent_repository_1 = require("./repositories/agent.repository");
const agent_secure_runtime_1 = require("./runtime/agent-secure-runtime");
const tool_registry_1 = require("./tool-registry");
function chatThreadId(agentId, userId) {
const baseThreadId = `${builder_tool_names_1.AGENT_THREAD_PREFIX.TEST}${agentId}`;
return userId ? `${baseThreadId}:${userId}` : baseThreadId;
}
let AgentsService = class AgentsService {
computeRuntimeCacheKey(params) {
if (params.usePublishedVersion) {
const parts = [params.agentId, 'published'];
if (params.integrationType)
parts.push(params.integrationType);
return parts.join(':');
}
const parts = [params.agentId, 'draft'];
if (params.n8nUserId)
parts.push(params.n8nUserId);
return parts.join(':');
}
clearRuntimes(agentId, options = {}) {
for (const key of this.runtimes.keys()) {
if (key === agentId || key.startsWith(`${agentId}:`)) {
this.runtimes.delete(key);
}
}
if (options.skipBroadcast)
return;
if (!this.globalConfig.multiMainSetup.enabled)
return;
void this.publisher
.publishCommand({
command: 'agent-config-changed',
payload: { agentId },
})
.catch((error) => {
this.logger.warn(`[AgentsService] Failed to publish agent-config-changed for ${agentId}`, {
error: error instanceof Error ? error.message : String(error),
});
});
}
handleAgentConfigChanged(payload) {
this.clearRuntimes(payload.agentId, { skipBroadcast: true });
}
constructor(logger, agentRepository, projectRelationRepository, workflowRunner, activeExecutions, executionRepository, workflowRepository, userRepository, workflowFinderService, urlService, n8nCheckpointStorage, secureRuntime, ephemeralNodeExecutor, agentsToolsService, n8nMemory, agentExecutionService, agentPublishedVersionRepository, agentSkillsService, publisher, agentsConfig, globalConfig) {
this.logger = logger;
this.agentRepository = agentRepository;
this.projectRelationRepository = projectRelationRepository;
this.workflowRunner = workflowRunner;
this.activeExecutions = activeExecutions;
this.executionRepository = executionRepository;
this.workflowRepository = workflowRepository;
this.userRepository = userRepository;
this.workflowFinderService = workflowFinderService;
this.urlService = urlService;
this.n8nCheckpointStorage = n8nCheckpointStorage;
this.secureRuntime = secureRuntime;
this.ephemeralNodeExecutor = ephemeralNodeExecutor;
this.agentsToolsService = agentsToolsService;
this.n8nMemory = n8nMemory;
this.agentExecutionService = agentExecutionService;
this.agentPublishedVersionRepository = agentPublishedVersionRepository;
this.agentSkillsService = agentSkillsService;
this.publisher = publisher;
this.agentsConfig = agentsConfig;
this.globalConfig = globalConfig;
this.runtimes = new ttl_map_1.TtlMap(30 * constants_1.Time.minutes.toMilliseconds);
}
isNodeToolsModuleEnabled() {
return this.agentsConfig.modules.includes('node-tools-searcher');
}
shouldAttachNodeTools(config) {
return this.isNodeToolsModuleEnabled() && (0, agent_json_config_1.isNodeToolsEnabled)(config);
}
listChatIntegrations() {
return di_1.Container.get(agent_chat_integration_1.ChatIntegrationRegistry)
.list()
.map((i) => ({
type: i.type,
label: i.displayLabel,
icon: i.displayIcon,
credentialTypes: i.credentialTypes,
}));
}
async create(projectId, name) {
const defaultConfig = {
name,
model: 'anthropic/claude-sonnet-4-5',
credential: '',
instructions: '',
tools: [],
skills: [],
};
const agent = this.agentRepository.create({
name,
projectId,
schema: defaultConfig,
versionId: (0, uuid_1.v4)(),
});
const saved = await this.agentRepository.save(agent);
this.logger.debug('Created SDK agent', { agentId: saved.id, projectId });
return saved;
}
async findByProjectId(projectId) {
return await this.agentRepository.findByProjectId(projectId);
}
async findById(agentId, projectId) {
return await this.agentRepository.findByIdAndProjectId(agentId, projectId);
}
async updateName(agentId, projectId, name) {
const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agent) {
return null;
}
agent.name = name;
if (agent.schema) {
agent.schema = { ...agent.schema, name };
}
(0, agent_draft_utils_1.markAgentDraftDirty)(agent);
const saved = await this.agentRepository.save(agent);
this.logger.debug('Updated SDK agent name', { agentId, projectId, name });
return saved;
}
async updateDescription(agentId, projectId, description, updatedAt) {
const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agent) {
return null;
}
if (updatedAt && agent.updatedAt.toISOString() !== updatedAt) {
throw new conflict_error_1.ConflictError('Agent has been modified');
}
agent.description = description;
if (agent.schema) {
agent.schema = { ...agent.schema, description };
}
(0, agent_draft_utils_1.markAgentDraftDirty)(agent);
const saved = await this.agentRepository.save(agent);
this.logger.debug('Updated SDK agent description', { agentId, projectId });
return saved;
}
async findByUser(userId) {
const projectRelations = await this.projectRelationRepository.findAllByUser(userId);
const projectIds = projectRelations.map((pr) => pr.projectId);
if (projectIds.length === 0)
return [];
return await this.agentRepository.find({
where: { projectId: (0, typeorm_1.In)(projectIds) },
order: { updatedAt: 'DESC' },
});
}
async findPublishedByUser(userId) {
const projectRelations = await this.projectRelationRepository.findAllByUser(userId);
const projectIds = projectRelations.map((pr) => pr.projectId);
if (projectIds.length === 0)
return [];
const agents = await this.agentRepository.find({
where: { projectId: (0, typeorm_1.In)(projectIds) },
relations: { publishedVersion: true },
order: { updatedAt: 'DESC' },
});
return agents.filter((agent) => agent.publishedVersion);
}
async publishAgent(agentId, projectId, userId) {
const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agent) {
throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`);
}
await this.agentRepository.manager.transaction(async (trx) => {
if (!agent.versionId) {
agent.versionId = (0, uuid_1.v4)();
await trx.save(agent);
}
agent.publishedVersion = await this.agentPublishedVersionRepository.savePublishedVersion({
agentId: agent.id,
schema: agent.schema,
tools: this.snapshotConfiguredTools(agent.schema, agent.tools ?? {}),
skills: this.agentSkillsService.snapshotConfiguredSkills(agent.schema, agent.skills ?? {}),
publishedFromVersionId: agent.versionId,
model: agent.model,
provider: agent.provider,
credentialId: agent.credentialId,
publishedById: userId,
}, trx);
});
this.clearRuntimes(agentId);
const credentialIntegrations = (agent.integrations ?? []).filter(api_types_1.isAgentCredentialIntegration);
if (credentialIntegrations.length > 0) {
const { ChatIntegrationService } = await Promise.resolve().then(() => __importStar(require('./integrations/chat-integration.service')));
await di_1.Container.get(ChatIntegrationService)
.syncToConfig(agent, [], credentialIntegrations)
.catch((error) => this.logger.warn('Failed to connect integrations on publish', {
agentId,
error,
}));
}
this.logger.debug('Published SDK agent', { agentId, projectId, userId });
return agent;
}
async unpublishAgent(agentId, projectId) {
const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agent) {
throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`);
}
await this.agentRepository.manager.transaction(async (trx) => {
await this.agentPublishedVersionRepository.deleteByAgentId(agentId, trx);
agent.publishedVersion = null;
const hasActiveSchedule = (agent.integrations ?? []).some((integration) => (0, api_types_1.isAgentScheduleIntegration)(integration) && integration.active);
if (hasActiveSchedule) {
agent.integrations = (agent.integrations ?? []).map((integration) => (0, api_types_1.isAgentScheduleIntegration)(integration) ? { ...integration, active: false } : integration);
await trx.save(agent);
}
});
this.clearRuntimes(agentId);
const { ChatIntegrationService } = await Promise.resolve().then(() => __importStar(require('./integrations/chat-integration.service')));
await di_1.Container.get(ChatIntegrationService).disconnect(agentId);
const { AgentScheduleService } = await Promise.resolve().then(() => __importStar(require('./integrations/agent-schedule.service')));
di_1.Container.get(AgentScheduleService).deregister(agentId);
this.logger.debug('Unpublished SDK agent', { agentId, projectId });
return agent;
}
async revertToPublishedAgent(agentId, projectId) {
const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agent) {
throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`);
}
const publishedVersion = agent.publishedVersion;
if (!publishedVersion) {
throw new conflict_error_1.ConflictError(`Agent "${agentId}" is not published`);
}
await this.agentRepository.manager.transaction(async (trx) => {
agent.schema = publishedVersion.schema ? (0, n8n_workflow_1.deepCopy)(publishedVersion.schema) : null;
agent.tools = (0, n8n_workflow_1.deepCopy)(publishedVersion.tools ?? {});
agent.skills = (0, n8n_workflow_1.deepCopy)(publishedVersion.skills ?? {});
agent.model = publishedVersion.model;
agent.provider = publishedVersion.provider;
agent.credentialId = publishedVersion.credentialId;
agent.versionId = publishedVersion.publishedFromVersionId;
if (agent.schema) {
agent.name = agent.schema.name;
agent.description = agent.schema.description ?? null;
}
await trx.save(agent);
});
this.clearRuntimes(agentId);
this.logger.debug('Reverted SDK agent to published version', { agentId, projectId });
return agent;
}
async delete(agentId, projectId) {
const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agent) {
return false;
}
await this.agentRepository.remove(agent);
this.clearRuntimes(agentId);
try {
const { AgentScheduleService } = await Promise.resolve().then(() => __importStar(require('./integrations/agent-schedule.service')));
di_1.Container.get(AgentScheduleService).deregister(agentId);
}
catch (error) {
this.logger.warn('Failed to stop schedule on agent delete', {
agentId,
error: error instanceof Error ? error.message : error,
});
}
try {
await this.clearAllTestChatMessages(agentId);
}
catch (error) {
this.logger.warn('Failed to clear test chat on agent delete', {
agentId,
error: error instanceof Error ? error.message : error,
});
}
this.logger.debug('Deleted SDK agent', { agentId, projectId });
return true;
}
async getChatMessages(threadId) {
return await this.n8nMemory.getMessages(threadId);
}
getMemoryFactory() {
return (params) => {
if (params.storage === 'n8n') {
return this.n8nMemory;
}
if (params.storage === 'sqlite') {
return new agents.SqliteMemory(agents.SqliteMemoryConfigSchema.parse(params));
}
throw new Error(`Unsupported memory storage: ${params.storage}`);
};
}
createCredentialProvider(projectId) {
return new agents_credential_provider_1.AgentsCredentialProvider(di_1.Container.get(credentials_service_1.CredentialsService), projectId);
}
async getRuntime(params) {
const { agentId, projectId, integrationType, usePublishedVersion } = params;
const cacheKey = this.computeRuntimeCacheKey(params);
const cached = this.runtimes.get(cacheKey);
if (cached)
return cached;
const agentEntity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agentEntity)
throw new not_found_error_1.NotFoundError(`Agent ${agentId} not found`);
let n8nUserId = params.n8nUserId;
let agentData = agentEntity;
if (usePublishedVersion) {
const publishedSchema = agentEntity.publishedVersion?.schema;
if (!publishedSchema) {
throw new not_found_error_1.NotFoundError(`Agent ${agentId} is not published`);
}
agentData = {
...agentEntity,
schema: publishedSchema,
tools: agentEntity.publishedVersion?.tools ?? agentEntity.tools ?? {},
skills: agentEntity.publishedVersion?.skills ?? agentEntity.skills ?? {},
};
n8nUserId ??= agentEntity.publishedVersion?.publishedById ?? undefined;
}
if (!n8nUserId) {
throw new n8n_workflow_1.UserError('Agent user owner id is required');
}
const credentialProvider = this.createCredentialProvider(projectId);
const { agent: agentInstance, toolRegistry } = await this.reconstructFromConfig(agentData, credentialProvider, n8nUserId, integrationType);
this.runtimes.set(cacheKey, { agent: agentInstance, agentId, toolRegistry, projectId });
const runtime = this.runtimes.get(cacheKey);
if (!runtime)
throw new Error(`Agent ${agentId} failed to reconstruct`);
return runtime;
}
makeToolResolver(projectId, userId) {
return async (ref) => {
if (ref.type === 'workflow') {
if (!userId) {
throw new n8n_workflow_1.UserError('userId is required when agent uses workflow tools');
}
const { resolveWorkflowTool } = await Promise.resolve().then(() => __importStar(require('./tools/workflow-tool-factory')));
return await resolveWorkflowTool(ref, {
workflowRepository: this.workflowRepository,
workflowRunner: this.workflowRunner,
activeExecutions: this.activeExecutions,
executionRepository: this.executionRepository,
workflowFinderService: this.workflowFinderService,
userRepository: this.userRepository,
userId,
projectId,
webhookBaseUrl: this.urlService.getWebhookBaseUrl(),
});
}
if (ref.type === 'node') {
const { resolveNodeTool } = await Promise.resolve().then(() => __importStar(require('./tools/node-tool-factory')));
return await resolveNodeTool(ref, {
executor: this.ephemeralNodeExecutor,
projectId,
});
}
return null;
};
}
async injectRuntimeDependencies(params) {
const { agent, agentId, projectId, credentialProvider, nodeToolsEnabled, integrationType } = params;
try {
const { createGetEnvironmentTool } = await Promise.resolve().then(() => __importStar(require('./tools/environment-tool')));
agent.tool(createGetEnvironmentTool());
}
catch (toolError) {
this.logger.warn('Failed to inject get_environment tool', {
agentId,
error: toolError instanceof Error ? toolError.message : String(toolError),
});
}
const integration = integrationType
? di_1.Container.get(agent_chat_integration_1.ChatIntegrationRegistry).get(integrationType)
: undefined;
if (integration?.supportedComponents !== undefined) {
try {
const { createRichInteractionTool } = await Promise.resolve().then(() => __importStar(require('./integrations/rich-interaction-tool')));
agent.tool(createRichInteractionTool(integrationType));
}
catch (toolError) {
this.logger.warn('Failed to inject rich_interaction tool', {
agentId,
error: toolError instanceof Error ? toolError.message : String(toolError),
});
}
}
if (nodeToolsEnabled) {
this.attachNodeToolChain(agent, credentialProvider, projectId);
}
if (!agent.hasCheckpointStorage()) {
agent.checkpoint(this.n8nCheckpointStorage);
}
}
attachNodeToolChain(agent, credentialProvider, projectId) {
agent.tool(this.agentsToolsService.getRuntimeTools(credentialProvider, projectId));
}
async *resumeForChat(config) {
const { agentId, projectId, runId, toolCallId, resumeData, integrationType } = config;
const checkpointStatus = await this.n8nCheckpointStorage.getStatus(runId);
if (checkpointStatus.status === 'expired') {
throw new n8n_workflow_1.UserError(`Checkpoint ${runId} is expired and cannot be resumed`);
}
if (checkpointStatus.status === 'not-found') {
throw new n8n_workflow_1.UserError(`Checkpoint ${runId} not found and cannot be resumed`);
}
const memoryScope = checkpointStatus.checkpoint?.persistence;
if (!memoryScope) {
throw new n8n_workflow_1.UserError(`Checkpoint ${runId} has no memory data and cannot be resumed`);
}
const threadId = memoryScope.threadId;
const runtime = await this.getRuntime({
agentId,
projectId,
usePublishedVersion: true,
integrationType,
});
const { agent: agentInstance, toolRegistry } = runtime;
const recorder = new execution_recorder_1.ExecutionRecorder(toolRegistry);
const resultStream = await agentInstance.resume('stream', resumeData, {
runId,
toolCallId,
});
const reader = resultStream.stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
recorder.record(value);
yield value;
}
}
finally {
reader.releaseLock();
}
const messageRecord = recorder.getMessageRecord();
void this.agentExecutionService
.recordMessage({
threadId,
agentId,
agentName: agentInstance.name,
projectId,
userMessage: '',
record: messageRecord,
hitlStatus: 'resumed',
})
.catch((error) => {
this.logger.warn('Failed to record resumed agent execution', {
agentId,
threadId,
error: error instanceof Error ? error.message : String(error),
});
});
}
async validateAgentIsRunnable(agentId, projectId, credentialProvider) {
const agentEntity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agentEntity) {
return { missing: ['agent'] };
}
const config = agentEntity.schema;
const missing = [];
if (!config) {
return { missing: ['instructions', 'model'] };
}
if (!config.instructions?.trim()) {
missing.push('instructions');
}
const modelSchema = agent_json_config_1.AgentJsonConfigSchema.shape.model;
if (!config.model || !modelSchema.safeParse(config.model).success) {
missing.push('model');
}
if (config.credential) {
try {
const credentialName = config.credential;
const creds = await credentialProvider.list();
const exists = creds.some((c) => c.id === credentialName || c.name.toLowerCase() === credentialName.toLowerCase());
if (!exists)
missing.push('credential');
}
catch {
}
}
missing.push(...this.agentSkillsService
.getMissingSkillIds(config, agentEntity.skills ?? {})
.map((skillId) => `skill:${skillId}`));
return { missing };
}
async *executeForChat(config) {
const { agentId, projectId, message, userId, memory } = config;
const runtime = await this.getRuntime({ agentId, projectId, n8nUserId: userId });
yield* this.streamChatResponse({
agentInstance: runtime.agent,
toolRegistry: runtime.toolRegistry,
agentId,
message,
memory,
projectId: runtime.projectId,
});
}
async getTestChatMessages(agentId, userId) {
return await this.n8nMemory.getMessages(chatThreadId(agentId, userId), {
resourceId: userId,
});
}
async clearTestChatMessages(agentId, userId) {
await this.n8nMemory.deleteMessagesByThread(chatThreadId(agentId, userId), userId);
}
async clearAllTestChatMessages(agentId) {
const threadId = chatThreadId(agentId);
await this.n8nMemory.deleteThreadsByPrefix(threadId);
await this.n8nMemory.deleteMessagesByThread(threadId);
await this.n8nMemory.deleteThread(threadId);
}
async *executeForChatPublished(config) {
const { agentId, projectId, message, memory, integrationType } = config;
const runtime = await this.getRuntime({
agentId,
projectId,
integrationType,
usePublishedVersion: true,
});
yield* this.streamChatResponse({
agentInstance: runtime.agent,
toolRegistry: runtime.toolRegistry,
agentId,
message,
memory,
projectId: runtime.projectId,
source: integrationType,
});
}
async *executeForSchedulePublished(config) {
const { agentId, projectId, message, memory } = config;
const runtime = await this.getRuntime({
agentId,
projectId,
integrationType: api_types_1.AGENT_SCHEDULE_TRIGGER_TYPE,
usePublishedVersion: true,
});
yield* this.streamChatResponse({
agentInstance: runtime.agent,
toolRegistry: runtime.toolRegistry,
agentId,
message,
memory,
projectId: runtime.projectId,
source: api_types_1.AGENT_SCHEDULE_TRIGGER_TYPE,
});
}
async *streamChatResponse(config) {
const { agentInstance, toolRegistry, agentId, message, memory, projectId, source } = config;
const { threadId, resourceId } = memory;
const recorder = new execution_recorder_1.ExecutionRecorder(toolRegistry);
const resultStream = await agentInstance.stream(message, {
persistence: { threadId, resourceId },
});
const reader = resultStream.stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
recorder.record(value);
if (value.type === 'tool-call-suspended') {
this.logger.info('Chat: tool-call-suspended chunk received', {
agentId,
toolCallId: value.toolCallId,
toolName: value.toolName,
});
}
yield value;
}
}
finally {
reader.releaseLock();
}
const messageRecord = recorder.getMessageRecord();
void this.agentExecutionService
.recordMessage({
threadId,
agentId,
agentName: agentInstance.name,
projectId,
userMessage: message,
record: messageRecord,
hitlStatus: recorder.suspended ? 'suspended' : undefined,
source,
})
.catch((error) => {
this.logger.warn('Failed to record agent execution', {
agentId,
threadId,
error: error instanceof Error ? error.message : String(error),
});
});
}
async compileIsolated(agentEntity, credentialProvider, userId) {
if (!agentEntity.schema) {
return { ok: false, error: 'Agent has no JSON config. Create a config first.' };
}
try {
const { agent: reconstructed } = await this.reconstructFromConfig(agentEntity, credentialProvider, userId);
return { ok: true, agent: reconstructed };
}
catch (e) {
return {
ok: false,
error: e instanceof Error ? e.message : 'Unknown compilation error',
};
}
}
async executeForWorkflow(agentId, message, executionId, threadId, userId, projectId) {
const agentEntity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agentEntity) {
throw new n8n_workflow_1.OperationalError('Agent not found or not accessible.');
}
if (!agentEntity.publishedVersion) {
throw new n8n_workflow_1.OperationalError('Agent is not published. Publish the agent before using it in a workflow.');
}
const credentialProvider = new agents_credential_provider_1.AgentsCredentialProvider(di_1.Container.get(credentials_service_1.CredentialsService), projectId);
const compiled = await this.compileIsolated(agentEntity, credentialProvider, userId);
if (!compiled.ok || !compiled.agent) {
throw new n8n_workflow_1.OperationalError(`Failed to compile agent: ${compiled.error ?? 'unknown error'}`);
}
const agentInstance = compiled.agent;
const recorder = new execution_recorder_1.ExecutionRecorder();
let structuredOutput = null;
const toolCalls = [];
const toolInputs = new Map();
const resultStream = await agentInstance.stream(message, {
persistence: { resourceId: executionId, threadId },
});
const reader = resultStream.stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
recorder.record(value);
if (value.type === 'tool-call') {
toolInputs.set(value.toolCallId, { toolName: value.toolName, input: value.input });
}
else if (value.type === 'tool-result') {
const pending = toolInputs.get(value.toolCallId);
toolCalls.push({
toolName: value.toolName,
input: pending?.input ?? null,
result: value.output,
});
toolInputs.delete(value.toolCallId);
}
else if (value.type === 'finish' && value.structuredOutput !== undefined) {
structuredOutput = value.structuredOutput;
}
}
}
finally {
reader.releaseLock();
}
const messageRecord = recorder.getMessageRecord();
void this.agentExecutionService
.recordMessage({
threadId,
agentId,
agentName: agentInstance.name,
projectId,
userMessage: message,
record: messageRecord,
source: api_types_1.AGENT_WORKFLOW_TRIGGER_TYPE,
})
.catch((error) => {
this.logger.warn('Failed to record agent execution from workflow', {
agentId,
threadId,
error: error instanceof Error ? error.message : String(error),
});
});
if (recorder.suspended) {
throw new n8n_workflow_1.OperationalError('Agent execution suspended waiting for tool approval. ' +
'Suspend/resume is not supported in workflow execution context.');
}
if (messageRecord.error) {
throw new n8n_workflow_1.OperationalError(`Agent execution failed: ${messageRecord.error}`);
}
if (messageRecord.finishReason === 'error') {
throw new n8n_workflow_1.OperationalError('Agent execution finished with an error.');
}
return {
response: messageRecord.assistantResponse,
structuredOutput: structuredOutput ?? null,
usage: messageRecord.usage
? {
promptTokens: messageRecord.usage.promptTokens,
completionTokens: messageRecord.usage.completionTokens,
totalTokens: messageRecord.usage.totalTokens,
}
: null,
toolCalls,
finishReason: messageRecord.finishReason,
session: {
agentId,
projectId,
sessionId: threadId,
},
};
}
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) {
const parsed = agent_json_config_1.AgentJsonConfigSchema.safeParse(raw);
if (!parsed.success) {
return { valid: false, error: parsed.error.message };
}
const config = parsed.data;
if ((0, agent_json_config_1.isNodeToolsEnabled)(config.config) && !this.isNodeToolsModuleEnabled()) {
return {
valid: false,
error: 'config.nodeTools.enabled requires the node-tools-searcher agents module to be enabled.',
};
}
try {
this.validateNodeToolExpressions(config);
}
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 this.validateNodeToolConfigs(config);
if (nodeError) {
return { valid: false, error: nodeError };
}
return { valid: true, config };
}
validateNodeToolExpressions(config) {
for (const tool of config.tools ?? []) {
if (tool.type !== 'node')
continue;
(0, ai_utilities_1.extractFromAIParameters)((tool.node.nodeParameters ?? {}));
}
}
async healIntegrationCredentialNames(rawConfig) {
if (!rawConfig || typeof rawConfig !== 'object')
return rawConfig;
const cfg = rawConfig;
if (!Array.isArray(cfg.integrations))
return rawConfig;
const missingIds = new Set();
for (const integration of cfg.integrations) {
if (!integration || typeof integration !== 'object')
continue;
const i = integration;
if (typeof i.credentialId === 'string' && i.credentialName === undefined) {
missingIds.add(i.credentialId);
}
}
if (missingIds.size === 0)
return rawConfig;
const credentials = await di_1.Container.get(db_1.CredentialsRepository).findBy({
id: (0, typeorm_1.In)(Array.from(missingIds)),
});
const namesById = new Map(credentials.map((c) => [c.id, c.name]));
const integrations = cfg.integrations;
return {
...cfg,
integrations: integrations.map((integration) => {
if (!integration || typeof integration !== 'object')
return integration;
const i = integration;
if (typeof i.credentialId !== 'string' || i.credentialName !== undefined) {
return integration;
}
const name = namesById.get(i.credentialId);
return name ? { ...integration, credentialName: name } : integration;
}),
};
}
async updateConfig(agentId, projectId, config) {
const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!entity)
throw new not_found_error_1.NotFoundError('Agent not found');
const healedConfig = await this.healIntegrationCredentialNames(config);
const result = await this.validateConfig(healedConfig);
if (!result.valid) {
throw new n8n_workflow_1.UserError(`Invalid agent config: ${result.error}`);
}
this.validateConfigRefs(result.config, entity);
const previousIntegrations = entity.integrations ?? [];
const previousSchema = entity.schema ?? null;
const integrationsProvided = result.config.integrations !== undefined;
const toolsProvided = result.config.tools !== undefined;
const skillsProvided = result.config.skills !== undefined;
const descriptionProvided = result.config.description !== undefined;
const credentialProvided = result.config.credential !== undefined;
const memoryProvided = result.config.memory !== undefined;
const providerToolsProvided = result.config.providerTools !== undefined;
const configBlockProvided = result.config.config !== undefined;
const { schemaConfig: decomposedSchema, integrations: decomposedIntegrations } = (0, agent_config_composition_1.decomposeJsonConfig)(result.config);
const nextIntegrations = integrationsProvided ? decomposedIntegrations : previousIntegrations;
const nextSchema = {
...(previousSchema ?? {}),
name: decomposedSchema.name,
model: decomposedSchema.model,
instructions: decomposedSchema.instructions,
...(descriptionProvided ? { description: decomposedSchema.description } : {}),
...(credentialProvided ? { credential: decomposedSchema.credential } : {}),
...(memoryProvided ? { memory: decomposedSchema.memory } : {}),
...(toolsProvided ? { tools: decomposedSchema.tools } : {}),
...(skillsProvided ? { skills: decomposedSchema.skills } : {}),
...(providerToolsProvided ? { providerTools: decomposedSchema.providerTools } : {}),
...(configBlockProvided ? { config: decomposedSchema.config } : {}),
};
entity.schema = nextSchema;
entity.name = result.config.name;
if (descriptionProvided)
entity.description = result.config.description ?? null;
entity.integrations = nextIntegrations;
(0, agent_draft_utils_1.markAgentDraftDirty)(entity);
if (toolsProvided) {
const referencedIds = new Set((result.config.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, result.config);
}
this.clearRuntimes(agentId);
const saved = await this.agentRepository.save(entity);
this.logger.debug('Updated agent JSON config', { agentId, projectId });
if (integrationsProvided) {
await (0, integrations_sync_1.syncAgentIntegrations)(saved, previousIntegrations, nextIntegrations, this.logger);
}
return {
config: (0, agent_config_composition_1.composeJsonConfig)(saved) ?? result.config,
updatedAt: saved.updatedAt.toISOString(),
versionId: saved.versionId,
};
}
async buildCustomTool(agentId, projectId, code, descriptor) {
const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!entity)
throw new not_found_error_1.NotFoundError('Agent not found');
const toolId = (0, agent_resource_id_1.generateAgentResourceId)('tool', Object.keys(entity.tools ?? {}));
entity.tools = {
...entity.tools,
[toolId]: { code, descriptor },
};
(0, agent_draft_utils_1.markAgentDraftDirty)(entity);
this.clearRuntimes(agentId);
await this.agentRepository.save(entity);
this.logger.debug('Built custom tool', { agentId, projectId, toolId });
return { ok: true, id: toolId, descriptor };
}
async listSkills(agentId, projectId) {
return await this.agentSkillsService.listSkills(agentId, projectId);
}
async getSkill(agentId, projectId, skillId) {
return await this.agentSkillsService.getSkill(agentId, projectId, skillId);
}
async createSkill(agentId, projectId, skill) {
const result = await this.agentSkillsService.createSkill(agentId, projectId, skill);
this.clearRuntimes(agentId);
return result;
}
async createAndAttachSkill(agentId, projectId, skill) {
const result = await this.agentSkillsService.createAndAttachSkill(agentId, projectId, skill);
this.clearRuntimes(agentId);
return result;
}
async updateSkill(agentId, projectId, skillId, updates) {
const result = await this.agentSkillsService.updateSkill(agentId, projectId, skillId, updates);
this.clearRuntimes(agentId);
return result;
}
async deleteCustomTool(agentId, projectId, toolId) {
const entity = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!entity)
throw new not_found_error_1.NotFoundError('Agent not found');
const tools = { ...entity.tools };
delete tools[toolId];
entity.tools = tools;
if (entity.schema?.tools) {
entity.schema.tools = entity.schema.tools.filter((t) => !(t.type === 'custom' && 'id' in t && t.id === toolId));
}
(0, agent_draft_utils_1.markAgentDraftDirty)(entity);
this.clearRuntimes(agentId);
await this.agentRepository.save(entity);
this.logger.debug('Deleted custom tool', { agentId, projectId, toolId });
}
async deleteSkill(agentId, projectId, skillId) {
await this.agentSkillsService.deleteSkill(agentId, projectId, skillId);
this.clearRuntimes(agentId);
}
async validateNodeToolConfigs(config) {
const nodeTools = (config.tools ?? []).filter((t) => t.type === 'node');
if (nodeTools.length === 0)
return null;
const { setSchemaBaseDirs, validateNodeConfig } = await Promise.resolve().then(() => __importStar(require('@n8n/workflow-sdk')));
const dirs = (0, node_definition_resolver_1.resolveBuiltinNodeDefinitionDirs)();
if (dirs.length > 0) {
setSchemaBaseDirs(dirs);
}
const errors = [];
for (const tool of nodeTools) {
const nodeType = tool.node.nodeType;
const nodeTypeVersion = tool.node.nodeTypeVersion;
const nodeParameters = tool.node.nodeParameters ?? {};
const result = validateNodeConfig(nodeType, nodeTypeVersion, { parameters: nodeParameters }, { isToolNode: true });
if (!result.valid) {
const messages = result.errors
.map((e) => e.message)
.join('; ');
errors.push(`Node tool "${tool.name}" (${nodeType}@${nodeTypeVersion}): ${messages}`);
}
}
return errors.length > 0 ? errors.join('\n') : null;
}
validateConfigRefs(config, entity) {
const missingSkillIds = this.agentSkillsService.getMissingSkillIds(config, entity.skills ?? {});
if (missingSkillIds.length > 0) {
throw new n8n_workflow_1.UserError(`Invalid agent config: Missing skill bodies: ${missingSkillIds.join(', ')}`);
}
const missingToolIds = this.getMissingCustomToolIds(config, entity.tools ?? {});
if (missingToolIds.length > 0) {
throw new n8n_workflow_1.UserError(`Invalid agent config: Missing custom tool definitions: ${missingToolIds.join(', ')}`);
}
const activeUnpublishedSchedule = (config.integrations ?? []).some((integration) => (0, api_types_1.isAgentScheduleIntegration)(integration) && integration.active);
if (activeUnpublishedSchedule && !entity.publishedVersion) {
throw new n8n_workflow_1.UserError('Invalid agent config: schedule integration cannot be active until the agent is published');
}
}
getMissingCustomToolIds(config, tools) {
const refs = (config?.tools ?? []).filter((ref) => ref.type === 'custom');
const seen = new Set();
const missing = [];
for (const ref of refs) {
if (seen.has(ref.id))
continue;
seen.add(ref.id);
if (!tools[ref.id])
missing.push(ref.id);
}
return missing;
}
snapshotConfiguredTools(config, tools) {
if (!config)
return null;
const missing = this.getMissingCustomToolIds(config, tools);
if (missing.length > 0) {
throw new n8n_workflow_1.UserError(`Cannot publish agent with missing custom tools: ${missing.join(', ')}`);
}
const snapshot = {};
for (const ref of config.tools ?? []) {
if (ref.type !== 'custom')
continue;
const tool = tools[ref.id];
if (tool)
snapshot[ref.id] = tool;
}
return snapshot;
}
async reconstructFromConfig(agentEntity, credentialProvider, userId, integrationType) {
const config = agentEntity.schema;
if (!config) {
throw new n8n_workflow_1.UserError('Agent has no JSON config.');
}
const toolsByName = {};
for (const [_toolId, toolEntry] of Object.entries(agentEntity.tools ?? {})) {
toolsByName[toolEntry.descriptor.name] = toolEntry.code;
}
const toolDescriptors = {};
for (const [toolId, toolEntry] of Object.entries(agentEntity.tools ?? {})) {
toolDescriptors[toolId] = toolEntry.descriptor;
}
const toolExecutor = this.secureRuntime.createToolExecutor(toolsByName);
const toolResolver = this.makeToolResolver(agentEntity.projectId, userId);
const resolvedTools = [];
const reconstructed = await (0, from_json_config_1.buildFromJson)(config, toolDescriptors, {
t