@salesforce/agents
Version:
Client side APIs for working with Salesforce agents
386 lines • 17.5 kB
JavaScript
"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