@salesforce/agents
Version:
Client side APIs for working with Salesforce agents
297 lines • 12.9 kB
JavaScript
"use strict";
/*
* Copyright 2025, 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentSimulate = void 0;
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const node_crypto_1 = require("node:crypto");
const core_1 = require("@salesforce/core");
const kit_1 = require("@salesforce/kit");
const agent_1 = require("./agent");
const agentPreviewBase_1 = require("./agentPreviewBase");
const apexUtils_1 = require("./apexUtils");
const utils_1 = require("./utils");
/**
* A service to simulate interactions with an agent using a local .agent file.
* The file will be compiled using Agent.compileAgent before being used
* with the simulation endpoints.
*
* **Examples**
*
* Create an instance of the service:
*
* `const agentSimulate = new AgentSimulate(connection, './path/to/agent.agent');`
*
* Start an interactive session:
*
* `const { sessionId } = await agentSimulate.start();`
*
* Send a message to the agent using the session ID from the startResponse:
*
* `const sendResponse = await agentSimulate.send(sessionId, message);`
*
* End an interactive session:
*
* `await agentSimulate.end(sessionId, 'UserRequest');`
*
* Enable Apex Debug Mode:
*
* `agentSimulate.toggleApexDebugMode(true);`
*/
class AgentSimulate extends agentPreviewBase_1.AgentPreviewBase {
/**
* The client can specify whether the actions will run in a simulated mode ("mock actions", no side effects, mockActions=true) or a non-simulated mode ("real actions", actions with side effects, mockActions=false)
*/
mockActions;
apiBase = `https://${kit_1.env.getBoolean('SF_TEST_API') ? 'test.' : ''}api.salesforce.com/einstein/ai-agent`;
agentFilePath;
compiledAgent;
/**
* Create an instance of the service.
*
* @param connection The connection to use to make requests.
* @param agentFilePath Path to the .agent file to simulate.
* @param mockActions whether or not to mock the actions of the simulated agent
*/
constructor(connection, agentFilePath, mockActions) {
super({ connection });
this.agentFilePath = agentFilePath;
this.mockActions = mockActions;
}
/**
* Start an interactive simulation session with the agent.
* This will first compile the agent script if it hasn't been compiled yet.
*
* @returns `AgentPreviewStartResponse`, which includes a session ID needed for other actions.
*/
async start() {
if (!this.compiledAgent) {
void core_1.Lifecycle.getInstance().emit('agents:compiling', {});
this.logger.debug(`Compiling agent script from ${this.agentFilePath}`);
const agentString = await (0, promises_1.readFile)(this.agentFilePath, 'utf-8');
const compiledAgent = await agent_1.Agent.compileAgentScript(this.connection, agentString);
if (compiledAgent.status === 'success' && compiledAgent.compiledArtifact) {
this.compiledAgent = compiledAgent.compiledArtifact;
// we must set the compiledAgent.agentVersion.developerName, we'll get this from the -meta <target> field
const metaContent = await (0, promises_1.readFile)(`${this.agentFilePath.replace('.agent', '.bundle-meta.xml')}`, 'utf-8');
this.compiledAgent.agentVersion.developerName = metaContent.match(/<target>.*(v\d+)<\/target>/)?.at(1) ?? 'v0';
}
else {
const formattedError = compiledAgent.errors
.map((e) => `- ${e.errorType} ${e.description}: ${e.lineStart}:${e.colStart} / ${e.lineEnd}:${e.colEnd}`)
.join('\n');
throw new core_1.SfError('Failed to compile agent script', formattedError);
}
}
this.logger.debug('Starting agent simulation session');
// send bypassUser=false when the compiledAgent.globalConfiguration.defaultAgentUser is INVALID
let bypassUser = (await this.connection.query(`SELECT Id FROM USER WHERE username='${this.compiledAgent.globalConfiguration.defaultAgentUser}'`)).totalSize === 1;
if (bypassUser && this.compiledAgent.globalConfiguration.agentType === 'AgentforceEmployeeAgent') {
// another situation which bypassUser = false, is when previewing an agent script, with a valid default_agent_user, and it's an AgentforceEmployeeAgent type
bypassUser = false;
}
const body = {
agentDefinition: this.compiledAgent,
enableSimulationMode: this.mockActions,
externalSessionKey: (0, node_crypto_1.randomUUID)(),
instanceConfig: {
endpoint: this.connection.instanceUrl,
},
variables: [],
parameters: {},
streamingCapabilities: {
chunkTypes: ['Text', 'LightningChunk'],
},
richContentCapabilities: {},
bypassUser,
executionHistory: [],
conversationContext: [],
};
try {
void core_1.Lifecycle.getInstance().emit('agents:simulation-starting', {});
this.connection = await (0, utils_1.useNamedUserJwt)(this.connection);
const response = await this.connection.request({
method: 'POST',
url: `${this.apiBase}/v1.1/preview/sessions`,
headers: {
'x-attributed-client': 'no-builder', // <- removes markdown from responses
'x-client-name': 'afdx',
},
body: JSON.stringify(body),
}, { retry: { maxRetries: 3 } });
const agentIdForStorage = (0, node_path_1.basename)(this.agentFilePath);
await (0, utils_1.appendTranscriptEntry)({
timestamp: new Date().toISOString(),
agentId: agentIdForStorage,
sessionId: response.sessionId,
role: 'agent',
text: response.messages.map((m) => m.message).join('\n'),
raw: response.messages,
}, true);
return response;
}
catch (err) {
throw core_1.SfError.wrap(err);
}
finally {
// Always restore the original connection, even if an error occurred
delete this.connection.accessToken;
await this.connection.refreshAuth();
}
}
/**
* Send a message to the agent using the session ID obtained by calling `start()`.
*
* @param sessionId A session ID provided by first calling `agentSimulate.start()`.
* @param message A message to send to the agent.
* @returns `AgentPreviewSendResponse`
*/
async send(sessionId, message) {
if (!this.compiledAgent) {
throw new core_1.SfError('Agent not compiled, please call .start() first');
}
const url = `${this.apiBase}/v1.1/preview/sessions/${sessionId}/messages`;
const body = {
message: {
sequenceId: Date.now(),
type: 'Text',
text: message,
},
variables: [],
};
this.logger.debug(`Sending message with apexDebugMode ${this.apexDebugMode ? 'enabled' : 'disabled'}`);
const agentIdForStorage = (0, node_path_1.basename)(this.agentFilePath);
try {
const start = Date.now();
if (this.apexDebugMode && !this.mockActions) {
await this.ensureTraceFlag();
}
await (0, utils_1.appendTranscriptEntry)({
timestamp: new Date().toISOString(),
agentId: agentIdForStorage,
sessionId,
role: 'user',
text: message,
});
this.connection = await (0, utils_1.useNamedUserJwt)(this.connection);
const response = await this.connection.request({
method: 'POST',
url,
body: JSON.stringify(body),
headers: {
'x-client-name': 'afdx',
},
});
await (0, utils_1.appendTranscriptEntry)({
timestamp: new Date().toISOString(),
agentId: agentIdForStorage,
sessionId,
role: 'agent',
text: response.messages.map((m) => m.message).join('\n'),
raw: response.messages,
});
if (this.apexDebugMode && !this.mockActions) {
const apexLog = await (0, apexUtils_1.getDebugLog)(this.connection, start, Date.now());
if (apexLog) {
if (apexLog.Id)
this.logger.debug(`Apex debug log ID for message is ${apexLog.Id}`);
response.apexDebugLog = apexLog;
}
else {
this.logger.debug('No apex debug log found for this message');
}
}
return response;
}
catch (err) {
throw core_1.SfError.wrap(err);
}
finally {
// Always restore the original connection, even if an error occurred
delete this.connection.accessToken;
await this.connection.refreshAuth();
}
}
/**
* Ending is not required, or supported, for AgentSimulation
* this is a noop method to support easier consumer typings
*
* @returns `AgentPreviewEndResponse`
*/
// eslint-disable-next-line class-methods-use-this
async end() {
return Promise.resolve({ messages: [], _links: [] });
}
/**
* Enable or disable Apex Debug Mode, which will enable trace flags for the Bot user
* and create apex debug logs for use within VS Code's Apex Replay Debugger.
*
* @param enable Whether to enable or disable Apex Debug Mode.
*/
toggleApexDebugMode(enable) {
this.setApexDebugMode(enable);
}
async trace(sessionId, messageId) {
const traces = await this.traces(sessionId, [messageId]);
return traces.length ? traces[0] : null;
}
async traces(sessionId, messageIds) {
try {
this.connection = await (0, utils_1.useNamedUserJwt)(this.connection);
const traces = [];
const tracePromises = messageIds.map((messageId) => this.connection.request({
method: 'GET',
url: `${this.apiBase}/v1.1/preview/sessions/${sessionId}/plans/${messageId}`,
headers: {
'x-client-name': 'afdx',
},
}));
const traceResults = await Promise.all(tracePromises);
traces.push(...traceResults);
return traces;
}
catch (err) {
throw core_1.SfError.wrap(err);
}
finally {
// Always restore the original connection, even if an error occurred
delete this.connection.accessToken;
await this.connection.refreshAuth();
}
}
// once we're previewing agents in the org, with mockActions = false, we'll have to figure out how to get the correct user that was simulated for apex invocattion
async ensureTraceFlag() {
if (this.apexTraceFlag) {
const expDate = this.apexTraceFlag.ExpirationDate;
if (expDate && new Date(expDate) > new Date()) {
this.logger.debug(`Using cached apexTraceFlag with ExpirationDate of ${expDate}`);
return;
}
else {
this.logger.debug('Cached apex trace flag is expired');
}
}
const user = this.compiledAgent?.globalConfiguration.defaultAgentUser ?? this.connection.getUsername();
const userId = (await this.connection.singleRecordQuery(`SELECT Id FROM User WHERE Username = '${user}'`)).Id;
this.apexTraceFlag = await (0, apexUtils_1.findTraceFlag)(this.connection, userId);
if (!this.apexTraceFlag) {
await (0, apexUtils_1.createTraceFlag)(this.connection, userId);
}
}
}
exports.AgentSimulate = AgentSimulate;
//# sourceMappingURL=agentSimulate.js.map