Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@
"typedoc": "^0.28.17",
"typescript": "^5.9.3"
},
"resolutions": {
"string-width": "^4.2.3"
},
"engines": {
"node": ">=18.0.0"
},
Expand Down
105 changes: 28 additions & 77 deletions src/agents/productionAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
import { randomUUID } from 'node:crypto';
import { Messages, SfError } from '@salesforce/core';
import { env } from '@salesforce/kit';
import {
type AgentPreviewEndResponse,
AgentPreviewInterface,
Expand All @@ -35,7 +34,7 @@ import {
writeMetaFileToHistory,
updateMetadataEndTime,
writeTraceToHistory,
getEndpoint,
requestWithEndpointFallback,
getHistoryDir,
getAllHistory,
TranscriptEntry,
Expand All @@ -52,10 +51,11 @@ export class ProductionAgent extends AgentBase {
private botMetadata: BotMetadata | undefined;
private id: string | undefined;
private apiName: string | undefined;
private apiBase = `https://${getEndpoint()}api.salesforce.com/einstein/ai-agent/v1`;
private readonly apiBase: string;

public constructor(private options: ProductionAgentOptions) {
super(options.connection);
this.apiBase = 'https://api.salesforce.com/einstein/ai-agent/v1';
if (!options.apiNameOrId) {
throw messages.createError('missingAgentNameOrId');
}
Expand Down Expand Up @@ -215,29 +215,14 @@ export class ProductionAgent extends AgentBase {
this.historyDir
);

let response: AgentPreviewSendResponse;
try {
response = await this.connection.request<AgentPreviewSendResponse>({
method: 'POST',
url,
body: JSON.stringify(body),
headers: {
'x-client-name': 'afdx',
},
});
} catch (error) {
const errorName = (error as { name?: string })?.name ?? '';
if (errorName.includes('404')) {
throw SfError.create({
name: 'AgentApiNotFound',
message: `Preview Send API returned 404. SF_TEST_API=${
env.getBoolean('SF_TEST_API') ? 'true' : 'false'
} If targeting a test.api environment, set SF_TEST_API=true, otherwise it's false.`,
cause: error,
});
}
throw SfError.wrap(error);
}
const response = await requestWithEndpointFallback<AgentPreviewSendResponse>(this.connection, {
method: 'POST',
url,
body: JSON.stringify(body),
headers: {
'x-client-name': 'afdx',
},
});

const planId = response.messages.at(0)!.planId;
this.planIds.add(planId);
Expand All @@ -256,12 +241,8 @@ export class ProductionAgent extends AgentBase {

// Fetch and write trace immediately if available
if (planId) {
try {
const trace = await this.getTrace(planId);
await writeTraceToHistory(planId, trace, this.historyDir);
} catch (error) {
throw SfError.wrap(error);
}
const trace = await this.getTrace(planId);
await writeTraceToHistory(planId, trace, this.historyDir);
}

if (this.apexDebugging && this.canApexDebug()) {
Expand Down Expand Up @@ -326,29 +307,14 @@ export class ProductionAgent extends AgentBase {
};

try {
let response: AgentPreviewStartResponse;
try {
response = await this.connection.request<AgentPreviewStartResponse>({
method: 'POST',
url,
body: JSON.stringify(body),
headers: {
'x-client-name': 'afdx',
},
});
} catch (error) {
const errorName = (error as { name?: string })?.name ?? '';
if (errorName.includes('404')) {
throw SfError.create({
name: 'AgentApiNotFound',
message: `Preview Start API returned 404. SF_TEST_API=${
env.getBoolean('SF_TEST_API') ? 'true' : 'false'
} If targeting a test.api environment, set SF_TEST_API=true, otherwise it's false.`,
cause: error,
});
}
throw SfError.wrap(error);
}
const response = await requestWithEndpointFallback<AgentPreviewStartResponse>(this.connection, {
method: 'POST',
url,
body: JSON.stringify(body),
headers: {
'x-client-name': 'afdx',
},
});
this.sessionId = response.sessionId;

const agentId = this.id!;
Expand Down Expand Up @@ -407,28 +373,13 @@ export class ProductionAgent extends AgentBase {
const url = `${this.apiBase}/sessions/${this.sessionId}`;
try {
// https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-examples.html#end-session
let response: AgentPreviewEndResponse;
try {
response = await this.connection.request<AgentPreviewEndResponse>({
method: 'DELETE',
url,
headers: {
'x-session-end-reason': reason,
},
});
} catch (error) {
const errorName = (error as { name?: string })?.name ?? '';
if (errorName.includes('404')) {
throw SfError.create({
name: 'AgentApiNotFound',
message: `Preview End API returned 404. SF_TEST_API=${
env.getBoolean('SF_TEST_API') ? 'true' : 'false'
} If targeting a test.api environment, set SF_TEST_API=true, otherwise it's false.`,
cause: error,
});
}
throw SfError.wrap(error);
}
const response = await requestWithEndpointFallback<AgentPreviewEndResponse>(this.connection, {
method: 'DELETE',
url,
headers: {
'x-session-end-reason': reason,
},
});

// Write end entry immediately
if (this.historyDir) {
Expand Down
133 changes: 34 additions & 99 deletions src/agents/scriptAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { join } from 'node:path';
import { writeFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { Lifecycle, SfError, SfProject } from '@salesforce/core';
import { env } from '@salesforce/kit';
import {
AgentJson,
type AgentPreviewEndResponse,
Expand All @@ -40,7 +39,8 @@ import {
logSessionToIndex,
updateMetadataEndTime,
writeTraceToHistory,
getEndpoint,
getHttpStatusCode,
requestWithEndpointFallback,
findAuthoringBundle,
getHistoryDir,
TranscriptEntry,
Expand All @@ -52,43 +52,20 @@ import { generateAgentScript } from '../templates/agentScriptTemplate';
import { ScriptAgentPublisher } from './scriptAgentPublisher';
import { AgentBase } from './agentBase';

/**
* Extract HTTP status code from API errors. Supports:
* - ERROR_HTTP_404 / ERROR_HTTP_500 style (name, errorCode, or data.errorCode)
* - Numeric statusCode on error, cause, or response
*/
function getHttpStatusCode(err: unknown): number | undefined {
const e = err as {
name?: string;
errorCode?: string;
data?: { errorCode?: string };
statusCode?: number;
cause?: unknown;
response?: { statusCode?: number };
};
const codeStr = e?.name ?? e?.errorCode ?? e?.data?.errorCode;
if (typeof codeStr === 'string') {
const match = /ERROR_HTTP_(\d+)/i.exec(codeStr);
if (match) {
return Number.parseInt(match[1], 10);
}
}
return e?.statusCode ?? getHttpStatusCode(e?.cause) ?? e?.response?.statusCode;
}

export class ScriptAgent extends AgentBase {
public preview: AgentPreviewInterface & {
setMockMode: (mockMode: 'Mock' | 'Live Test') => void;
};
private mockMode: 'Mock' | 'Live Test' = 'Mock';
private agentScriptContent: AgentScriptContent;
private agentJson: AgentJson | undefined;
private apiBase = `https://${getEndpoint()}api.salesforce.com/einstein/ai-agent`;
private readonly apiBase: string;
private readonly aabDirectory: string;
private readonly metaContent: string;
public constructor(private options: ScriptAgentOptions) {
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();
Expand Down Expand Up @@ -187,27 +164,13 @@ export class ScriptAgent extends AgentBase {
}

public async getTrace(planId: string): Promise<PlannerResponse> {
try {
return await this.connection.request<PlannerResponse>({
method: 'GET',
url: `${this.apiBase}/v1.1/preview/sessions/${this.sessionId!}/plans/${planId}`,
headers: {
'x-client-name': 'afdx',
},
});
} catch (error) {
const errorName = (error as { name?: string })?.name ?? '';
if (errorName.includes('404')) {
throw SfError.create({
name: 'AgentApiNotFound',
message: `Trace API returned 404. SF_TEST_API=${
env.getBoolean('SF_TEST_API') ? 'true' : 'false'
} If targeting a test.api environment, set SF_TEST_API=true, otherwise it's false.`,
cause: error,
});
}
throw SfError.wrap(error);
}
return requestWithEndpointFallback<PlannerResponse>(this.connection, {
method: 'GET',
url: `${this.apiBase}/v1.1/preview/sessions/${this.sessionId!}/plans/${planId}`,
headers: {
'x-client-name': 'afdx',
},
});
}

/**
Expand All @@ -217,7 +180,7 @@ export class ScriptAgent extends AgentBase {
* @beta
*/
public async compile(): Promise<CompileAgentScriptResponse> {
const url = `https://${getEndpoint()}api.salesforce.com/einstein/ai-agent/v1.1/authoring/scripts`;
const url = `${this.apiBase}/v1.1/authoring/scripts`;

const compileData = {
assets: [
Expand All @@ -236,7 +199,8 @@ export class ScriptAgent extends AgentBase {
};

try {
const response = await this.connection.request<CompileAgentScriptResponse>(
const response = await requestWithEndpointFallback<CompileAgentScriptResponse>(
this.connection,
{
method: 'POST',
url,
Expand All @@ -254,17 +218,14 @@ export class ScriptAgent extends AgentBase {

return response;
} catch (error) {
const statusCode = getHttpStatusCode(error);
if (statusCode === 404) {
throw SfError.create({
name: 'AgentApiNotFound',
message: `Validation API returned 404. SF_TEST_API=${
env.getBoolean('SF_TEST_API') ? 'true' : 'false'
} If targeting a test.api environment, set SF_TEST_API=true, otherwise it's false.`,
cause: error,
exitCode: COMPILATION_API_EXIT_CODES.NOT_FOUND,
});
// Check if it's a 404 from our fallback function
const sfError = error as SfError;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to wrap than cast.

Suggested change
const sfError = error as SfError;
const sfError = SfError.wrap(error);

if (sfError.name === 'AgentApiNotFound') {
sfError.exitCode = COMPILATION_API_EXIT_CODES.NOT_FOUND;
throw sfError;
}
// Handle 500 errors specially
const statusCode = getHttpStatusCode(error);
const wrapped = SfError.wrap(error);
if (statusCode === 500) {
wrapped.exitCode = COMPILATION_API_EXIT_CODES.SERVER_ERROR;
Expand Down Expand Up @@ -395,29 +356,14 @@ export class ScriptAgent extends AgentBase {
this.historyDir
);

let response: AgentPreviewSendResponse;
try {
response = await this.connection.request<AgentPreviewSendResponse>({
method: 'POST',
url,
body: JSON.stringify(body),
headers: {
'x-client-name': 'afdx',
},
});
} catch (error) {
const errorName = (error as { name?: string })?.name ?? '';
if (errorName.includes('404')) {
throw SfError.create({
name: 'AgentApiNotFound',
message: `Preview Send API returned 404. SF_TEST_API=${
env.getBoolean('SF_TEST_API') ? 'true' : 'false'
} If targeting a test.api environment, set SF_TEST_API=true, otherwise it's false.`,
cause: error,
});
}
throw SfError.wrap(error);
}
const response = await requestWithEndpointFallback<AgentPreviewSendResponse>(this.connection, {
method: 'POST',
url,
body: JSON.stringify(body),
headers: {
'x-client-name': 'afdx',
},
});

const planId = response.messages.at(0)!.planId;
this.planIds.add(planId);
Expand All @@ -436,12 +382,8 @@ export class ScriptAgent extends AgentBase {

// Fetch and write trace immediately if available
if (planId) {
try {
const trace = await this.getTrace(planId);
await writeTraceToHistory(planId, trace, this.historyDir);
} catch (error) {
throw SfError.wrap(error);
}
const trace = await this.getTrace(planId);
await writeTraceToHistory(planId, trace, this.historyDir);
}

if (this.apexDebugging && this.canApexDebug()) {
Expand Down Expand Up @@ -521,7 +463,8 @@ export class ScriptAgent extends AgentBase {

let response: AgentPreviewStartResponse;
try {
response = await this.connection.request<AgentPreviewStartResponse>(
response = await requestWithEndpointFallback<AgentPreviewStartResponse>(
this.connection,
{
method: 'POST',
url: `${this.apiBase}/v1.1/preview/sessions`,
Expand All @@ -534,16 +477,8 @@ export class ScriptAgent extends AgentBase {
{ retry: { maxRetries: 3 } }
);
} catch (error) {
// If it's not a 404, add custom error handling
const err = SfError.wrap(error);
if (err.name.includes('404')) {
throw SfError.create({
name: 'AgentApiNotFound',
message: `Preview Start API returned 404. SF_TEST_API=${
env.getBoolean('SF_TEST_API') ? 'true' : 'false'
} If targeting a test.api environment, set SF_TEST_API=true, otherwise it's false.`,
cause: error,
});
}
const stackToCheck = (err.cause as Error)?.stack ?? err.stack;
if (this.mockMode === 'Live Test' && stackToCheck?.includes('Internal Error')) {
err.message =
Expand Down
Loading
Loading