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
8 changes: 8 additions & 0 deletions messages/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ 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.
76 changes: 73 additions & 3 deletions src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
type AgentJobSpec,
type AgentJobSpecCreateConfig,
type AgentOptions,
type BotActivationResponse,
type BotMetadata,
type BotVersionMetadata,
type DraftAgentTopicsBody,
type DraftAgentTopicsResponse,
} from './types.js';
Expand Down Expand Up @@ -69,7 +71,7 @@ export class Agent {
private id?: string;
// The name of the agent (Bot)
private name?: string;
// The metadata fields for the agent (Bot)
// The metadata fields for the agent (Bot and BotVersion)
private botMetadata?: BotMetadata;

/**
Expand Down Expand Up @@ -122,6 +124,19 @@ export class Agent {
return bots;
}

/**
* Lists all agents in the org.
*
* @param connection a `Connection` to an org.
* @returns the list of agents
*/
public static async listRemote(connection: Connection): Promise<BotMetadata[]> {
const agentsQuery = await connection.query<BotMetadata>(
'SELECT FIELDS(ALL), (SELECT FIELDS(ALL) FROM BotVersions LIMIT 10) FROM BotDefinition LIMIT 100'
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason to limit to 10/100?

);
return agentsQuery.records;
}

/**
* Creates an agent from a configuration, optionally saving the agent in an org.
*
Expand Down Expand Up @@ -269,21 +284,76 @@ export class Agent {
}

/**
* Queries BotDefinition for the bot metadata and assigns:
* Queries BotDefinition and BotVersions (limited to 10) for the bot metadata and assigns:
* 1. this.id
* 2. this.name
* 3. this.botMetadata
* 4. this.botVersionMetadata
*/
public async getBotMetadata(): Promise<BotMetadata> {
if (!this.botMetadata) {
const whereClause = this.id ? `Id = '${this.id}'` : `DeveloperName = '${this.name as string}'`;
const query = `SELECT FIELDS(ALL) FROM BotDefinition WHERE ${whereClause} LIMIT 1`;
const query = `SELECT FIELDS(ALL), (SELECT FIELDS(ALL) FROM BotVersions LIMIT 10) FROM BotDefinition WHERE ${whereClause} LIMIT 1`;
this.botMetadata = await this.options.connection.singleRecordQuery<BotMetadata>(query);
this.id = this.botMetadata.Id;
this.name = this.botMetadata.DeveloperName;
}
return this.botMetadata;
}

/**
* Returns the latest bot version metadata.
*
* @returns the latest bot version metadata
*/
public async getLatestBotVersionMetadata(): Promise<BotVersionMetadata> {
if (!this.botMetadata) {
this.botMetadata = await this.getBotMetadata();
}
const botVersions = this.botMetadata.BotVersions.records;
return botVersions[botVersions.length - 1];
}

/**
* Activates the agent.
*
* @returns void
*/
public async activate(): Promise<void> {
return this.setAgentStatus('Active');
}

/**
* Deactivates the agent.
*
* @returns void
*/
public async deactivate(): Promise<void> {
return this.setAgentStatus('Inactive');
}

private async setAgentStatus(desiredState: 'Active' | 'Inactive'): Promise<void> {
const botMetadata = await this.getBotMetadata();
const botVersionMetadata = await this.getLatestBotVersionMetadata();

if (botMetadata.IsDeleted) {
throw messages.createError('agentIsDeleted', [botMetadata.DeveloperName]);
}

if (botVersionMetadata.Status === desiredState) {
getLogger().debug(`Agent ${botMetadata.DeveloperName} is already ${desiredState}. Nothing to do.`);
return;
}

const url = `/connect/bot-versions/${botVersionMetadata.Id}/activation`;
const maybeMock = new MaybeMock(this.options.connection);
const response = await maybeMock.request<BotActivationResponse>('POST', url, { status: desiredState });
if (response.success) {
this.botMetadata!.BotVersions.records[0].Status = response.isActivated ? 'Active' : 'Inactive';
} else {
throw messages.createError('agentActivationError', [response.messages?.toString() ?? 'unknown']);
}
}
}

// private function used by Agent.createSpec()
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export {
type AgentPreviewStartResponse,
type AgentPreviewSendResponse,
type AgentPreviewEndResponse,
type BotMetadata,
type BotVersionMetadata,
type DraftAgentTopics,
type DraftAgentTopicsBody,
type DraftAgentTopicsResponse,
Expand Down
26 changes: 25 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type AgentOptions = {

export type BotMetadata = {
Id: string;
IsDeleted: false;
IsDeleted: boolean;
DeveloperName: string;
MasterLabel: string;
CreatedDate: string; // eg., "2025-02-13T18:25:17.000+0000",
Expand All @@ -40,6 +40,30 @@ export type BotMetadata = {
Type: string;
AgentType: string;
AgentTemplate: null | string;
BotVersions: { records: BotVersionMetadata[] };
};

export type BotVersionMetadata = {
Id: string;
Status: 'Active' | 'Inactive';
IsDeleted: boolean;
BotDefinitionId: string;
DeveloperName: string;
CreatedDate: string; // eg., "2025-06-02T23:16:20.000+0000",
CreatedById: string;
LastModifiedDate: string; // eg., "2025-06-02T23:16:21.000+0000",
LastModifiedById: string;
SystemModstamp: string; // eg., "2025-06-02T23:16:21.000+0000",
VersionNumber: number;
CopilotPrimaryLanguage: null | string;
ToneType: AgentTone;
CopilotSecondaryLanguages: null | string[];
};

export type BotActivationResponse = {
success: boolean;
isActivated: boolean;
messages?: string[];
};

/**
Expand Down
99 changes: 81 additions & 18 deletions test/nuts/agent.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('agent NUTs', () => {
await session?.clean();
});

describe('getBotMetadata()', () => {
describe('List and Get Bot Metadata', () => {
let botId: string;
const botApiName = 'Local_Info_Agent';

Expand Down Expand Up @@ -98,27 +98,90 @@ describe('agent NUTs', () => {
console.dir(deployResult.response, { depth: 10 });
}
expect(deployResult.response.success, 'expected deploy to succeed').to.equal(true);

// wait for the agent to be provisioned
console.log('\nwaiting 2 minutes for agent to be provisioned...');
await sleep(120_000);
});

describe('getBotMetadata()', () => {
it('should get agent bot metadata by bot developer name', async () => {
const agent = new Agent({ connection, nameOrId: botApiName });
const botMetadata = await agent.getBotMetadata();
expect(botMetadata).to.be.an('object');
expect(botMetadata.Id).to.be.a('string');
expect(botMetadata.BotUserId).to.be.a('string');
expect(botMetadata.AgentType).to.equal('EinsteinServiceAgent');
expect(botMetadata.DeveloperName).to.equal(botApiName);
expect(botMetadata.BotVersions.records.length).to.equal(1);
botId = botMetadata.Id;
expect(botMetadata.BotVersions.records[0].BotDefinitionId).to.equal(botId);
});

it('should get agent bot metadata by botId', async () => {
const agent = new Agent({ connection, nameOrId: botId });
const botMetadata = await agent.getBotMetadata();
expect(botMetadata).to.be.an('object');
expect(botMetadata.Id).to.equal(botId);
expect(botMetadata.BotUserId).to.be.a('string');
expect(botMetadata.AgentType).to.equal('EinsteinServiceAgent');
expect(botMetadata.DeveloperName).to.equal(botApiName);
expect(botMetadata.BotVersions.records.length).to.equal(1);
expect(botMetadata.BotVersions.records[0].BotDefinitionId).to.equal(botId);
});
});

describe('getLatestBotVersionMetadata()', () => {
it('should get the latest agent bot version metadata by bot developer name', async () => {
const agent = new Agent({ connection, nameOrId: botApiName });
const botVersionMetadata = await agent.getLatestBotVersionMetadata();
expect(botVersionMetadata).to.be.an('object');
expect(botVersionMetadata.Id).to.be.a('string');
expect(botVersionMetadata.Status).to.be.a('string');
expect(botVersionMetadata.IsDeleted).to.equal(false);
expect(botVersionMetadata.DeveloperName).to.equal('v1');
expect(botVersionMetadata.BotDefinitionId).to.equal(botId);
});
});

it('should get agent bot metadata by bot developer name', async () => {
const agent = new Agent({ connection, nameOrId: botApiName });
const botMetadata = await agent.getBotMetadata();
expect(botMetadata).to.be.an('object');
expect(botMetadata.Id).to.be.a('string');
expect(botMetadata.BotUserId).to.be.a('string');
expect(botMetadata.AgentType).to.equal('EinsteinServiceAgent');
expect(botMetadata.DeveloperName).to.equal(botApiName);
botId = botMetadata.Id;
describe('listRemote()', () => {
it('should list all agents in the org', async () => {
const agents = await Agent.listRemote(connection);
expect(agents).to.be.an('array');
expect(agents.length).to.equal(1);
expect(agents[0].DeveloperName).to.equal(botApiName);
expect(agents[0].Id).to.equal(botId);
expect(agents[0].BotVersions.records.length).to.equal(1);
expect(agents[0].BotVersions.records[0].BotDefinitionId).to.equal(botId);
});
});

it('should get agent bot metadata by botId', async () => {
const agent = new Agent({ connection, nameOrId: botId });
const botMetadata = await agent.getBotMetadata();
expect(botMetadata).to.be.an('object');
expect(botMetadata.Id).to.equal(botId);
expect(botMetadata.BotUserId).to.be.a('string');
expect(botMetadata.AgentType).to.equal('EinsteinServiceAgent');
expect(botMetadata.DeveloperName).to.equal(botApiName);
describe('activate/deactivate', () => {
it('should activate the agent', async () => {
const agent = new Agent({ connection, nameOrId: botId });
let botMetadata = await agent.getBotMetadata();
expect(botMetadata.BotVersions.records[0].Status).to.equal('Inactive');
try {
await agent.activate();
} catch (err) {
const errMsg = err instanceof Error ? err.message : 'err';
console.log('error activating agent. Waiting 2 minutes and trying again.', errMsg);
await sleep(120_000);
await agent.activate();
}

botMetadata = await agent.getBotMetadata();
expect(botMetadata.BotVersions.records[0].Status).to.equal('Active');
});

it('should deactivate the agent', async () => {
const agent = new Agent({ connection, nameOrId: botId });
let botMetadata = await agent.getBotMetadata();
expect(botMetadata.BotVersions.records[0].Status).to.equal('Active');
await agent.deactivate();
botMetadata = await agent.getBotMetadata();
expect(botMetadata.BotVersions.records[0].Status).to.equal('Inactive');
});
});
});

Expand Down
Loading