UNPKG

@salesforce/agents

Version:

Client side APIs for working with Salesforce agents

481 lines 21.8 kB
"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