UNPKG

@salesforce/agents

Version:

Client side APIs for working with Salesforce agents

386 lines 17.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProductionAgent = void 0; /* * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const node_crypto_1 = require("node:crypto"); const core_1 = require("@salesforce/core"); const maybe_mock_1 = require("../maybe-mock"); const utils_1 = require("../utils"); const apexUtils_1 = require("../apexUtils"); const agentBase_1 = require("./agentBase"); ; const messages = new core_1.Messages('@salesforce/agents', 'agents', new Map([["invalidAgentSpecConfig", "Missing one or more of the required agent spec arguments: type, role, companyName, companyDescription"], ["missingAgentName", "The \"agentName\" configuration property is required when saving an agent."], ["agentRetrievalError", "Unable to retrieve newly created Agent metadata. Due to: %s"], ["agentRetrievalErrorActions", "Retrieve the agent metadata using the \"project retrieve start\" command."], ["missingAgentNameOrId", "The \"nameOrId\" agent option is required when creating an Agent instance."], ["agentIsDeleted", "The %s agent has been deleted and its activation state can't be changed."], ["agentActivationError", "Changing the agent's activation status was unsuccessful due to %s."], ["versionNotFound", "Version %s not found for this agent."], ["noActiveVersion", "No active version found for agent %s."], ["noVersionsFound", "No versions found for agent %s."]])); class ProductionAgent extends agentBase_1.AgentBase { options; preview; botMetadata; id; apiName; apiBase; constructor(options) { super(options.connection); this.options = options; this.apiBase = 'https://api.salesforce.com/einstein/ai-agent/v1'; if (!options.apiNameOrId) { throw messages.createError('missingAgentNameOrId'); } this.preview = { start: (apexDebugging) => this.startPreview(apexDebugging), send: (message) => this.sendMessage(message), getAllTraces: () => this.getAllTracesFromDisc(), end: (reason) => this.endSession(reason), saveSession: (outputDir) => this.saveSessionTo(outputDir), setApexDebugging: (apexDebugging) => this.setApexDebugging(apexDebugging), }; if (options.apiNameOrId.startsWith('0Xx') && [15, 18].includes(options.apiNameOrId.length)) { this.id = options.apiNameOrId; } else { this.apiName = options.apiNameOrId; } } async getBotMetadata() { if (!this.botMetadata) { const whereClause = this.id ? `Id = '${this.id}'` : `DeveloperName = '${this.apiName}'`; const botDefinitionFields = 'Id, IsDeleted, DeveloperName, MasterLabel, CreatedDate, CreatedById, LastModifiedDate, LastModifiedById, SystemModstamp, BotUserId, Description, Type, AgentType, AgentTemplate'; const botVersionFields = 'Id, Status, IsDeleted, BotDefinitionId, DeveloperName, CreatedDate, CreatedById, LastModifiedDate, LastModifiedById, SystemModstamp, VersionNumber, CopilotPrimaryLanguage, ToneType, CopilotSecondaryLanguages'; const standardConn = await this.getStandardConnection(); this.botMetadata = await standardConn.singleRecordQuery(`SELECT ${botDefinitionFields}, (SELECT ${botVersionFields} FROM BotVersions WHERE IsDeleted = false ORDER BY VersionNumber) FROM BotDefinition WHERE ${whereClause} LIMIT 1`); this.id = this.botMetadata.Id; this.apiName = this.botMetadata.DeveloperName; // Set the display name from MasterLabel this.name = this.botMetadata.MasterLabel; } return this.botMetadata; } /** * Returns the latest bot version metadata. * * @returns the latest bot version metadata */ async getLatestBotVersionMetadata() { if (!this.botMetadata) { this.botMetadata = await this.getBotMetadata(); } const botVersions = this.botMetadata.BotVersions.records; if (botVersions.length === 0) { throw messages.createError('noVersionsFound', [this.botMetadata.DeveloperName]); } return botVersions[botVersions.length - 1]; } /** * Gets a bot version by its version number, or latest if omitted. * Searches for a version with matching VersionNumber property. * * @param {number} version - The VersionNumber to find (e.g., 0, 1, 2, 31), or undefined for latest * @returns {Promise<BotVersionMetadata>} */ async getBotVersionMetadata(version) { if (!this.botMetadata) { this.botMetadata = await this.getBotMetadata(); } const botVersions = this.botMetadata.BotVersions.records; if (botVersions.length === 0) { throw messages.createError('noVersionsFound', [this.botMetadata.DeveloperName]); } // If no version specified, return the latest (last in array) if (version === undefined) { return botVersions[botVersions.length - 1]; } // Find the version by VersionNumber property const foundVersion = botVersions.find((v) => v.VersionNumber === version); if (!foundVersion) { throw messages.createError('versionNotFound', [version.toString()]); } return foundVersion; } // eslint-disable-next-line @typescript-eslint/require-await,class-methods-use-this,@typescript-eslint/no-unused-vars async getTrace(planId) { return undefined; } getHistoryFromDisc(sessionId) { // Use provided sessionId, or fall back to this.sessionId, or let getAllHistory find the most recent const actualSessionId = sessionId ?? this.sessionId; return (0, utils_1.getAllHistory)(this.getAgentIdForStorage(), actualSessionId); } /** * Returns the ID for this agent. * * @returns The ID of the agent (The `Bot` ID). */ async getId() { if (!this.id) { await this.getBotMetadata(); } return this.id; // getBotMetadata() ensures this.id is not undefined } /** * Activates the agent. * * @param version - The VersionNumber to activate (e.g., 0, 1, 2, 31), or undefined for latest * @returns The activated bot version metadata */ async activate(version) { return this.setAgentStatus('Active', version); } /** * Deactivates the currently active agent version. * Only one version can be active at a time, so this automatically finds and deactivates it. * * @returns The deactivated bot version metadata */ async deactivate() { const botMetadata = await this.getBotMetadata(); const activeVersion = botMetadata.BotVersions.records.find((v) => v.Status === 'Active'); if (!activeVersion) { throw messages.createError('noActiveVersion', [botMetadata.DeveloperName]); } return this.setAgentStatus('Inactive', activeVersion.VersionNumber); } getAgentIdForStorage() { if (!this.id) { throw core_1.SfError.create({ name: 'noId', message: 'Agent ID not found. Call .getBotMetadata() first.' }); } return this.id; } // eslint-disable-next-line class-methods-use-this canApexDebug() { return true; } async handleApexDebuggingSetup() { const botMetadata = await this.getBotMetadata(); if (botMetadata.BotUserId) { const standardConn = await this.getStandardConnection(); const traceFlag = await (0, apexUtils_1.findTraceFlag)(standardConn, botMetadata.BotUserId); if (!traceFlag) { await (0, apexUtils_1.createTraceFlag)(standardConn, botMetadata.BotUserId); } } } async sendMessage(message) { if (!this.sessionId) { throw core_1.SfError.create({ name: 'noSessionId', message: 'Agent not started, please call .start() first' }); } // Auto-resume session if historyBuffer is not initialized but sessionId is set if (!this.historyBuffer) { await this.resumeSession(this.sessionId); // Double-check that resumeSession succeeded if (!this.historyBuffer) { throw core_1.SfError.create({ name: 'noHistoryBuffer', message: 'Failed to resume session' }); } } const url = `${this.apiBase}/sessions/${this.sessionId}/messages`; const body = { message: { sequenceId: Date.now(), type: 'Text', text: message, }, variables: [], }; try { const start = Date.now(); // Handle Apex debugging setup if needed if (this.apexDebugging && this.canApexDebug()) { await this.handleApexDebuggingSetup(); } const agentId = this.getAgentIdForStorage(); // Ensure session directory exists if (!this.historyDir) { this.historyDir = await (0, utils_1.getHistoryDir)(agentId, this.sessionId); } const userEntry = { timestamp: new Date().toISOString(), agentId, sessionId: this.sessionId, role: 'user', text: message, }; await (0, utils_1.logTurnToHistory)(userEntry, ++this.turnCounter, this.historyDir, this.historyBuffer); const response = await (0, utils_1.requestWithEndpointFallback)(await this.getJwtConnection(), { method: 'POST', url, body: JSON.stringify(body), headers: { 'x-client-name': 'afdx', }, }); const planId = response.messages.at(0).planId; this.planIds.add(planId); const agentEntry = { timestamp: new Date().toISOString(), agentId, sessionId: this.sessionId, role: 'agent', text: response.messages.at(0)?.message, raw: response.messages, }; const agentTurn = ++this.turnCounter; await (0, utils_1.logTurnToHistory)(agentEntry, agentTurn, this.historyDir, this.historyBuffer); // Fetch and write trace immediately if available if (planId) { await (0, utils_1.recordTraceForTurn)(this.historyDir, agentTurn, planId, undefined, this.historyBuffer); } // Flush buffer to keep turn-index.json and metadata.json up to date await this.historyBuffer.flush(); if (this.apexDebugging && this.canApexDebug()) { const apexLog = await (0, apexUtils_1.getDebugLog)(await this.getStandardConnection(), start, Date.now()); if (apexLog) { response.apexDebugLog = apexLog; } } return response; } catch (err) { throw core_1.SfError.wrap(err); } } async setAgentStatus(desiredState, version) { const botMetadata = await this.getBotMetadata(); const botVersionMetadata = await this.getBotVersionMetadata(version); if (botMetadata.IsDeleted) { throw messages.createError('agentIsDeleted', [botMetadata.DeveloperName]); } if (botVersionMetadata.Status === desiredState) { return botVersionMetadata; } const url = `/connect/bot-versions/${botVersionMetadata.Id}/activation`; const maybeMock = new maybe_mock_1.MaybeMock(await this.getJwtConnection()); const response = await maybeMock.request('POST', url, { status: desiredState }); if (response.success) { const versionToUpdate = this.botMetadata.BotVersions.records.find((v) => v.DeveloperName === botVersionMetadata.DeveloperName); if (versionToUpdate) { versionToUpdate.Status = response.isActivated ? 'Active' : 'Inactive'; } } else { throw messages.createError('agentActivationError', [response.messages?.toString() ?? 'unknown']); } return botVersionMetadata; } async startPreview(apexDebugging) { if (!this.id) { await this.getId(); } const url = `${this.apiBase}/agents/${this.id}/sessions`; // Use the provided apexDebugging parameter if given, otherwise keep the previously set one if (apexDebugging !== undefined) { this.apexDebugging = apexDebugging; } const body = { externalSessionKey: (0, node_crypto_1.randomUUID)(), instanceConfig: { endpoint: this.options.connection.instanceUrl, }, streamingCapabilities: { chunkTypes: ['Text'], }, bypassUser: true, }; try { const response = await (0, utils_1.requestWithEndpointFallback)(await this.getJwtConnection(), { method: 'POST', url, body: JSON.stringify(body), headers: { 'x-client-name': 'afdx', }, }); this.sessionId = response.sessionId; const agentId = this.id; this.historyDir = await (0, utils_1.getHistoryDir)(agentId, response.sessionId); const startTime = new Date().toISOString(); // Initialize history buffer (no file I/O yet) this.historyBuffer = new utils_1.SessionHistoryBuffer(this.historyDir, response.sessionId, agentId, startTime); this.turnCounter = 0; const initialEntry = { timestamp: startTime, agentId, sessionId: response.sessionId, role: 'agent', text: response.messages.map((m) => m.message).join('\n'), raw: response.messages, }; await (0, utils_1.logTurnToHistory)(initialEntry, ++this.turnCounter, this.historyDir, this.historyBuffer); // Write turn-index.json and metadata.json immediately so they exist after session start await this.historyBuffer.flush(); const agentDir = await (0, utils_1.getAgentIndexDir)(agentId); await (0, utils_1.logSessionToIndex)(agentDir, { sessionId: response.sessionId, startTime, simulated: false, agentId, }); return response; } catch (err) { throw core_1.SfError.wrap(err); } } /** * Ends an interactive session with the agent. * * @param sessionId A session ID provided by first calling `agentPreview.start()`. * @param reason A reason why the interactive session was ended. * @returns `AgentPreviewEndResponse` */ async endSession(reason) { if (!this.sessionId) { throw core_1.SfError.create({ name: 'noSessionId', message: 'please call .start() first' }); } if (!this.id) { throw core_1.SfError.create({ name: 'noId', message: 'please call .getId() first' }); } const url = `${this.apiBase}/sessions/${this.sessionId}`; try { // https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-examples.html#end-session const response = await (0, utils_1.requestWithEndpointFallback)(await this.getJwtConnection(), { method: 'DELETE', url, headers: { 'x-session-end-reason': reason, }, }); // Write end entry and flush buffer if (this.historyDir && this.historyBuffer) { const endTime = new Date().toISOString(); const endEntry = { timestamp: endTime, agentId: this.id, sessionId: this.sessionId, role: 'agent', reason, raw: response.messages, }; await (0, utils_1.logTurnToHistory)(endEntry, ++this.turnCounter, this.historyDir, this.historyBuffer); // Flush all buffered data to disk (turn-index.json and metadata.json) await this.historyBuffer.flush(endTime); } // Clear session data for next session this.sessionId = undefined; this.historyDir = undefined; this.historyBuffer = undefined; this.planIds = new Set(); return response; } catch (err) { throw core_1.SfError.wrap(err); } } } exports.ProductionAgent = ProductionAgent; //# sourceMappingURL=productionAgent.js.map