@salesforce/agents
Version:
Client side APIs for working with Salesforce agents
481 lines • 21.8 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 __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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScriptAgent = 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_fs_1 = __importStar(require("node:fs"));
const node_path_1 = require("node:path");
const promises_1 = require("node:fs/promises");
const node_crypto_1 = require("node:crypto");
const core_1 = require("@salesforce/core");
const types_1 = require("../types");
const utils_1 = require("../utils");
const apexUtils_1 = require("../apexUtils");
const agentScriptTemplate_1 = require("../templates/agentScriptTemplate");
const stringReplacements_1 = require("../stringReplacements");
const scriptAgentPublisher_1 = require("./scriptAgentPublisher");
const agentBase_1 = require("./agentBase");
class ScriptAgent extends agentBase_1.AgentBase {
options;
preview;
mockMode = 'Mock';
agentScriptContent;
agentJson;
apiBase;
aabDirectory;
metaContent;
agentFilePath;
constructor(options) {
super(options.connection);
this.options = options;
this.apiBase = 'https://api.salesforce.com/einstein/ai-agent';
// Find the AAB directory using the project
const projectDirs = options.project.getPackageDirectories();
const searchDirs = projectDirs.map((pkgDir) => pkgDir.fullPath);
const foundDirectory = (0, utils_1.findAuthoringBundle)(searchDirs, options.aabName);
if (!foundDirectory) {
throw core_1.SfError.create({
name: 'AABNotFound',
message: `Cannot find an authoring bundle named '${options.aabName}' in the project. Searched in: ${searchDirs.join(', ')}`,
});
}
this.aabDirectory = foundDirectory;
// Set initial name from AAB name (will be updated when agent is compiled)
this.name = options.aabName;
// Load the .agent file
this.agentFilePath = (0, node_path_1.join)(this.aabDirectory, `${options.aabName}.agent`);
this.agentScriptContent = node_fs_1.default.readFileSync(this.agentFilePath, 'utf-8');
// Load and validate the bundle-meta.xml file
const bundleMetaPath = (0, node_path_1.join)(this.aabDirectory, `${options.aabName}.bundle-meta.xml`);
if (!(0, node_fs_1.existsSync)(bundleMetaPath)) {
throw core_1.SfError.create({
name: 'BundleMetaNotFound',
message: `Cannot find bundle-meta.xml file for '${options.aabName}' at ${bundleMetaPath}`,
});
}
this.metaContent = node_fs_1.default.readFileSync(bundleMetaPath, 'utf-8');
if (options.agentJson) {
this.agentJson = options.agentJson;
this.name = options.agentJson.globalConfiguration?.label?.trim() || options.aabName;
}
this.preview = {
start: (mockModeOrOptions, apexDebugging, startOptions) => {
if (typeof mockModeOrOptions === 'object' && mockModeOrOptions !== null) {
return this.startPreview(undefined, undefined, mockModeOrOptions);
}
return this.startPreview(mockModeOrOptions, apexDebugging, startOptions);
},
send: (message) => this.sendMessage(message),
getAllTraces: () => this.getAllTracesFromDisc(),
end: () => this.endSession(),
saveSession: (outputDir) => this.saveSessionTo(outputDir),
setMockMode: (mockMode) => this.setMockMode(mockMode),
setApexDebugging: (apexDebugging) => this.setApexDebugging(apexDebugging),
};
}
/**
* Creates an AiAuthoringBundle directory, .script file, and -meta.xml file
*
* @returns Promise<void>
* @beta
* @param options {
* project: SfProject;
* bundleApiName: string;
* outputDir?: string;
* agentSpec?: ExtendedAgentJobSpec;
*}
*/
static async createAuthoringBundle(options) {
// this will eventually be done via AI in the org, but for now, we're hardcoding a valid .agent file boilerplate response
const agentScript = (0, agentScriptTemplate_1.generateAgentScript)(options.bundleApiName, options.agentSpec);
// Get default output directory if not specified
const targetOutputDir = (0, node_path_1.join)(options.outputDir ?? (0, node_path_1.join)(options.project.getDefaultPackage().fullPath, 'main', 'default'), 'aiAuthoringBundles', options.bundleApiName);
(0, node_fs_1.mkdirSync)(targetOutputDir, { recursive: true });
// Generate file paths
const agentPath = (0, node_path_1.join)(targetOutputDir, `${options.bundleApiName}.agent`);
const metaXmlPath = (0, node_path_1.join)(targetOutputDir, `${options.bundleApiName}.bundle-meta.xml`);
// Write Agent file
await (0, promises_1.writeFile)(agentPath, agentScript);
// Write meta.xml file
const metaXml = `<?xml version="1.0" encoding="UTF-8"?>
<AiAuthoringBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<bundleType>AGENT</bundleType>
</AiAuthoringBundle>`;
await (0, promises_1.writeFile)(metaXmlPath, metaXml);
}
async refreshContent() {
this.agentScriptContent = await node_fs_1.default.promises.readFile(this.agentFilePath, 'utf-8');
await this.compile();
}
async getTrace(planId) {
return (0, utils_1.requestWithEndpointFallback)(await this.getJwtConnection(), {
method: 'GET',
url: `${this.apiBase}/v1.1/preview/sessions/${this.sessionId}/plans/${planId}`,
headers: {
'x-client-name': 'afdx',
},
});
}
/**
* Compiles AgentScript returning agent JSON when successful, otherwise the compile errors are returned.
*
* @returns Promise<CompileAgentScriptResponse> The raw API response
* @beta
*/
async compile() {
const url = `${this.apiBase}/v1.1/authoring/scripts`;
const compileData = {
assets: [
{
type: 'AFScript',
name: 'AFScript',
content: this.agentScriptContent,
},
],
afScriptVersion: '2.0.0',
};
const headers = {
'x-client-name': 'afdx',
'content-type': 'application/json',
};
try {
const response = await (0, utils_1.requestWithEndpointFallback)(await this.getJwtConnection(), {
method: 'POST',
url,
headers,
body: JSON.stringify(compileData),
}, { retry: { maxRetries: 3 } });
if (response.status === 'success') {
this.agentJson = response.compiledArtifact;
// Set the display name from agentJson label, or fallback to AAB name
this.name = this.agentJson.globalConfiguration.label || this.options.aabName;
}
return response;
}
catch (error) {
// Check if it's a 404 from our fallback function
const sfError = error;
if (sfError.name === 'AgentApiNotFound') {
sfError.exitCode = types_1.COMPILATION_API_EXIT_CODES.NOT_FOUND;
throw sfError;
}
// Handle 500 errors specially
const statusCode = (0, utils_1.getHttpStatusCode)(error);
const wrapped = core_1.SfError.wrap(error);
if (statusCode === 500) {
wrapped.exitCode = types_1.COMPILATION_API_EXIT_CODES.SERVER_ERROR;
}
throw wrapped;
}
}
/**
* Publish an AgentJson representation to the org
*
* @beta
* @returns {Promise<PublishAgentJsonResponse>} The publish response
*/
async publish(skipMetadataRetrieve) {
// Apply string replacements before compiling for publish
const originalContent = this.agentScriptContent;
try {
const replacementResult = await (0, stringReplacements_1.applyStringReplacementsToAgent)(this.agentFilePath, this.agentScriptContent, this.options.project);
// Temporarily replace content with the result of string replacements
this.agentScriptContent = replacementResult.content;
// Log replacements if any were made
if (replacementResult.replacementsMade > 0) {
void core_1.Lifecycle.getInstance().emit('agents:string-replacements-applied', {
replacements: replacementResult.replacements,
});
}
await this.compile();
}
finally {
// Always restore original content after compilation
this.agentScriptContent = originalContent;
}
const publisher = new scriptAgentPublisher_1.ScriptAgentPublisher(this.options.connection, this.options.project, this.agentJson, skipMetadataRetrieve);
return publisher.publishAgentJson();
}
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);
}
/**
* Ending is not required
* this will save all the transcripts to disc
*
* @returns `AgentPreviewEndResponse`
*/
async endSession() {
if (!this.sessionId) {
return Promise.resolve({ messages: [], _links: [] });
}
if (this.historyDir && this.historyBuffer) {
const endTime = new Date().toISOString();
const endEntry = {
timestamp: endTime,
agentId: this.getAgentIdForStorage(),
sessionId: this.sessionId,
role: 'agent',
reason: 'UserRequest',
raw: [],
};
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 Promise.resolve({ messages: [], _links: [] });
}
getAgentIdForStorage() {
return this.options.aabName;
}
canApexDebug() {
return this.mockMode === 'Live Test';
}
async handleApexDebuggingSetup() {
// ScriptAgent doesn't need trace flag setup for Apex debugging
// Apex debugging is handled differently for script agents
// Reference this to satisfy linter
void this;
return Promise.resolve();
}
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}/v1.1/preview/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);
}
await (0, utils_1.logTurnToHistory)({
timestamp: new Date().toISOString(),
agentId,
sessionId: this.sessionId,
role: 'user',
text: message,
}, ++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 agentTurn = ++this.turnCounter;
await (0, utils_1.logTurnToHistory)({
timestamp: new Date().toISOString(),
agentId,
sessionId: this.sessionId,
role: 'agent',
text: response.messages.at(0)?.message,
raw: response.messages,
}, agentTurn, this.historyDir, this.historyBuffer);
// Fetch and write trace immediately if available
if (planId) {
const trace = await this.getTrace(planId);
await (0, utils_1.recordTraceForTurn)(this.historyDir, agentTurn, planId, trace, 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);
}
}
setMockMode(mockMode) {
this.mockMode = mockMode;
}
async startPreview(mockMode, apexDebugging, options) {
if (!this.agentJson) {
void core_1.Lifecycle.getInstance().emit('agents:compiling', {});
await this.compile();
}
if (!this.agentJson) {
throw core_1.SfError.create({ message: 'error compiling', name: 'unable to start preview' });
}
// Use the provided mockMode parameter if given, otherwise keep the previously set one
if (mockMode !== undefined) {
this.mockMode = mockMode;
}
if (apexDebugging !== undefined) {
this.apexDebugging = apexDebugging;
}
// send bypassUser=false when the compiledAgent.globalConfiguration.defaultAgentUser is INVALID
const standardConn = await this.getStandardConnection();
let bypassUser = (await standardConn.query(`SELECT Id FROM USER WHERE username='${this.agentJson.globalConfiguration.defaultAgentUser}'`)).totalSize === 1;
if (bypassUser && this.agentJson.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 agentDefinition = this.agentJson;
agentDefinition.agentVersion.developerName = this.metaContent.match(/<target>.*(v\d+)<\/target>/)?.at(1) ?? 'v0';
const body = {
agentDefinition,
enableSimulationMode: this.mockMode === 'Mock',
externalSessionKey: (0, node_crypto_1.randomUUID)(),
instanceConfig: {
endpoint: this.options.connection.instanceUrl,
},
variables: options?.contextVariables ?? [],
parameters: {},
streamingCapabilities: {
chunkTypes: ['Text', 'LightningChunk'],
},
richContentCapabilities: {},
bypassUser,
executionHistory: [],
conversationContext: [],
};
try {
void core_1.Lifecycle.getInstance().emit('agents:simulation-starting', {});
let response;
try {
response = await (0, utils_1.requestWithEndpointFallback)(await this.getJwtConnection(), {
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 } });
}
catch (error) {
// If it's not a 404, add custom error handling
const err = core_1.SfError.wrap(error);
const stackToCheck = err.cause?.stack ?? err.stack;
if (this.mockMode === 'Live Test' && stackToCheck?.includes('Internal Error')) {
err.message =
"ensure the 'default_agent_user' set, is valid, and has the required permission sets assigned ['AgentforceServiceAgentBase', 'AgentforceServiceAgentUser', 'EinsteinGPTPromptTemplateUser']";
}
throw err;
}
this.sessionId = response.sessionId;
const agentIdForStorage = this.options.aabName;
// Initialize session directory and write initial data
// Session directory structure:
// .sfdx/agents/<agentId>/sessions/<sessionId>/
// ├── transcript.jsonl # All transcript entries (one per line)
// ├── traces/ # Individual trace files
// │ ├── <planId1>.json
// │ └── <planId2>.json
// └── metadata.json # Session metadata (start time, end time, planIds, etc.)
this.historyDir = await (0, utils_1.getHistoryDir)(agentIdForStorage, 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, agentIdForStorage, startTime, this.mockMode);
this.turnCounter = 0;
// Write initial agent messages immediately
const initialEntry = {
timestamp: startTime,
agentId: agentIdForStorage,
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)(agentIdForStorage);
await (0, utils_1.logSessionToIndex)(agentDir, {
sessionId: response.sessionId,
startTime,
simulated: this.mockMode === 'Mock',
agentId: agentIdForStorage,
});
return response;
}
catch (err) {
throw core_1.SfError.wrap(err);
}
}
}
exports.ScriptAgent = ScriptAgent;
//# sourceMappingURL=scriptAgent.js.map