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
12 changes: 10 additions & 2 deletions messages/agent.preview.sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ List all known programmatic agent preview sessions.

This command lists the agent preview sessions that were started with the "agent preview start" command and are still in the local cache. Use this command to discover specific session IDs that you can pass to the "agent preview send" or "agent preview end" commands with the --session-id flag.

Programmatic agent preview sessions can be started for both published activated agents and by using an agent's local authoring bundle, which contains its Agent Script file. In this command's output table, the Agent column contains either the API name of the authoring bundle or the published agent, whichever was used when starting the session. In the table, if the same API name has multiple rows with different session IDs, then it means that you previously started multiple preview sessions with the associated agent.
Programmatic agent preview sessions can be started for both published activated agents and by using an agent's local authoring bundle, which contains its Agent Script file. In this command's output table, the Agent column contains either the API name of the authoring bundle or the published agent, whichever was used when starting the session. In the table, if the same API name has multiple rows with different session IDs, then it means that you previously started multiple preview sessions with the associated agent.

# output.empty

Expand All @@ -20,8 +20,16 @@ Agent (authoring bundle or API name)

Session ID

# output.tableHeader.timestamp

Started At

# output.tableHeader.sessionType

Session Type

# examples

- List all cached agent preview sessions:

<%= config.bin %> <%= command.id %>
<%= config.bin %> <%= command.id %>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@inquirer/prompts": "^7.10.1",
"@oclif/core": "^4",
"@oclif/multi-stage-output": "^0.8.36",
"@salesforce/agents": "^1.1.2",
"@salesforce/agents": "^1.2.0",
"@salesforce/core": "^8.28.3",
"@salesforce/kit": "^3.2.6",
"@salesforce/sf-plugins-core": "^12.2.6",
Expand Down
10 changes: 10 additions & 0 deletions schemas/agent-preview-sessions.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,21 @@
},
"sessionId": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"sessionType": {
"$ref": "#/definitions/SessionType"
}
},
"required": ["agentId", "sessionId"],
"additionalProperties": false
}
},
"SessionType": {
"type": "string",
"enum": ["simulated", "live", "published"]
}
}
}
22 changes: 17 additions & 5 deletions src/commands/agent/preview/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@

import { SfCommand, toHelpSection } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { listCachedSessions } from '../../../previewSessionStore.js';
import { listCachedSessions, SessionType } from '../../../previewSessionStore.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.preview.sessions');

export type AgentPreviewSessionsResult = Array<{ agentId: string; displayName?: string; sessionId: string }>;
export type AgentPreviewSessionsResult = Array<{
agentId: string;
displayName?: string;
sessionId: string;
timestamp?: string;
sessionType?: SessionType;
}>;

export default class AgentPreviewSessions extends SfCommand<AgentPreviewSessionsResult> {
public static readonly summary = messages.getMessage('summary');
Expand All @@ -36,9 +42,9 @@ export default class AgentPreviewSessions extends SfCommand<AgentPreviewSessions
public async run(): Promise<AgentPreviewSessionsResult> {
const entries = await listCachedSessions(this.project!);
const rows: AgentPreviewSessionsResult = [];
for (const { agentId, displayName, sessionIds } of entries) {
for (const sessionId of sessionIds) {
rows.push({ agentId, displayName, sessionId });
for (const { agentId, displayName, sessions } of entries) {
for (const { sessionId, timestamp, sessionType } of sessions) {
rows.push({ agentId, displayName, sessionId, timestamp, sessionType });
}
}

Expand All @@ -53,15 +59,21 @@ export default class AgentPreviewSessions extends SfCommand<AgentPreviewSessions

const agentColumnHeader = messages.getMessage('output.tableHeader.agent');
const sessionIdHeader = messages.getMessage('output.tableHeader.sessionId');
const timestampHeader = messages.getMessage('output.tableHeader.timestamp');
const sessionTypeHeader = messages.getMessage('output.tableHeader.sessionType');
const tableData = rows.map((r) => ({
agent: r.displayName ?? r.agentId,
sessionId: r.sessionId,
timestamp: r.timestamp ?? '',
sessionType: r.sessionType ?? '',
}));
this.table({
data: tableData,
columns: [
{ key: 'agent', name: agentColumnHeader },
{ key: 'sessionId', name: sessionIdHeader },
{ key: 'timestamp', name: timestampHeader },
{ key: 'sessionType', name: sessionTypeHeader },
],
});
return rows;
Expand Down
12 changes: 9 additions & 3 deletions src/commands/agent/preview/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { Flags, SfCommand, toHelpSection } from '@salesforce/sf-plugins-core';
import { EnvironmentVariable, Lifecycle, Messages, SfError } from '@salesforce/core';
import { Agent, ProductionAgent, ScriptAgent } from '@salesforce/agents';
import { createCache } from '../../../previewSessionStore.js';
import { createCache, SessionType } from '../../../previewSessionStore.js';
import { COMPILATION_API_EXIT_CODES } from '../../../common.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
Expand Down Expand Up @@ -87,7 +87,7 @@ export default class AgentPreviewStart extends SfCommand<AgentPreviewStartResult
const agentIdentifier = flags['authoring-bundle'] ?? flags['api-name']!;

// Track telemetry for agent initialization
let agent;
let agent: ScriptAgent | ProductionAgent;
try {
agent = flags['authoring-bundle']
? await Agent.init({ connection: conn, project: this.project!, aabName: flags['authoring-bundle'] })
Expand Down Expand Up @@ -157,11 +157,17 @@ export default class AgentPreviewStart extends SfCommand<AgentPreviewStartResult
}

const displayName = flags['authoring-bundle'] ?? flags['api-name'];
await createCache(agent, { displayName });
const sessionType = resolveSessionType(agent, simulateActions);
await createCache(agent, { displayName, sessionType });

await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_start_success' });
const result: AgentPreviewStartResult = { sessionId: session.sessionId };
this.log(messages.getMessage('output.sessionId', [session.sessionId]));
return result;
}
}

function resolveSessionType(agent: ScriptAgent | ProductionAgent, simulateActions: boolean | undefined): SessionType {
if (agent instanceof ProductionAgent) return 'published';
return simulateActions ? 'simulated' : 'live';
}
168 changes: 12 additions & 156 deletions src/previewSessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,159 +14,15 @@
* limitations under the License.
*/

import { readdir, readFile, unlink, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { SfError } from '@salesforce/core';
import type { SfProject } from '@salesforce/core';
import type { ProductionAgent, ScriptAgent } from '@salesforce/agents';

const SESSION_META_FILE = 'session-meta.json';

export type SessionMeta = { displayName?: string };

/**
* Save a marker so send/end can validate that the session was started for this agent.
* Caller must have started the session (agent has sessionId set). Uses agent.getHistoryDir() for the path.
* Pass displayName (authoring bundle name or production agent API name) so "agent preview sessions" can show it.
*/
export async function createCache(
agent: ScriptAgent | ProductionAgent,
options?: { displayName?: string }
): Promise<void> {
const historyDir = await agent.getHistoryDir();
const metaPath = join(historyDir, SESSION_META_FILE);
const meta: SessionMeta = { displayName: options?.displayName };
await writeFile(metaPath, JSON.stringify(meta), 'utf-8');
}

/**
* Validate that the session was started for this agent (marker file exists in agent's history dir for current sessionId).
* Caller must set sessionId on the agent (agent.setSessionId) before calling.
* Throws SfError if the session marker is not found.
*/
export async function validatePreviewSession(agent: ScriptAgent | ProductionAgent): Promise<void> {
const historyDir = await agent.getHistoryDir();
const metaPath = join(historyDir, SESSION_META_FILE);
try {
await readFile(metaPath, 'utf-8');
} catch {
throw new SfError(
'No preview session found for this session ID. Run "sf agent preview start" first.',
'PreviewSessionNotFound'
);
}
}

/**
* Remove the session marker so this session is no longer considered "active" for send/end without --session-id.
* Call after ending the session. Caller must set sessionId on the agent before calling.
*/
export async function removeCache(agent: ScriptAgent | ProductionAgent): Promise<void> {
const historyDir = await agent.getHistoryDir();
const metaPath = join(historyDir, SESSION_META_FILE);
try {
await unlink(metaPath);
} catch {
// already removed or never created
}
}

/**
* List session IDs that have a cache marker (started via "agent preview start") for this agent.
* Uses project path and agent's storage ID to find .sfdx/agents/<agentId>/sessions/<sessionId>/session-meta.json.
*/
export async function getCachedSessionIds(project: SfProject, agent: ScriptAgent | ProductionAgent): Promise<string[]> {
const agentId = agent.getAgentIdForStorage();
const base = join(project.getPath(), '.sfdx');
const sessionsDir = join(base, 'agents', agentId, 'sessions');
const sessionIds: string[] = [];
try {
const entries = await readdir(sessionsDir, { withFileTypes: true });
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
const hasMarker = await Promise.all(
dirs.map(async (name) => {
try {
await readFile(join(sessionsDir, name, SESSION_META_FILE), 'utf-8');
return true;
} catch {
return false;
}
})
);
dirs.forEach((name, i) => {
if (hasMarker[i]) sessionIds.push(name);
});
} catch {
// sessions dir missing or unreadable
}
return sessionIds;
}

/**
* Return the single "current" session ID when safe: exactly one cached session for this agent.
* Returns undefined when there are zero or multiple sessions (caller should require --session-id).
*/
export async function getCurrentSessionId(
project: SfProject,
agent: ScriptAgent | ProductionAgent
): Promise<string | undefined> {
const ids = await getCachedSessionIds(project, agent);
return ids.length === 1 ? ids[0] : undefined;
}

export type CachedSessionEntry = { agentId: string; displayName?: string; sessionIds: string[] };

/**
* List all cached preview sessions in the project, grouped by agent ID.
* displayName (when present in session-meta.json) is the authoring bundle name or production agent API name for display.
* Use this to show users which sessions exist so they can end or clean up.
*/
export async function listCachedSessions(project: SfProject): Promise<CachedSessionEntry[]> {
const base = join(project.getPath(), '.sfdx', 'agents');
const result: CachedSessionEntry[] = [];
try {
const agentDirs = await readdir(base, { withFileTypes: true });
const entries = await Promise.all(
agentDirs
.filter((ent) => ent.isDirectory())
.map(async (ent) => {
const agentId = ent.name;
const sessionsDir = join(base, agentId, 'sessions');
let sessionIds: string[] = [];
let displayName: string | undefined;
try {
const sessionDirs = await readdir(sessionsDir, { withFileTypes: true });
const withMarker = await Promise.all(
sessionDirs
.filter((s) => s.isDirectory())
.map(async (s) => {
try {
await readFile(join(sessionsDir, s.name, SESSION_META_FILE), 'utf-8');
return s.name;
} catch {
return null;
}
})
);
sessionIds = withMarker.filter((id): id is string => id !== null);
if (sessionIds.length > 0) {
try {
const raw = await readFile(join(sessionsDir, sessionIds[0], SESSION_META_FILE), 'utf-8');
const meta = JSON.parse(raw) as SessionMeta;
displayName = meta.displayName;
} catch {
// ignore
}
}
} catch {
// no sessions dir or unreadable
}
return { agentId, displayName, sessionIds };
})
);
result.push(...entries.filter((e) => e.sessionIds.length > 0));
} catch {
// no agents dir or unreadable
}
return result;
}
export {
createPreviewSessionCache as createCache,
validatePreviewSession,
removePreviewSessionCache as removeCache,
getCachedPreviewSessionIds as getCachedSessionIds,
getCurrentPreviewSessionId as getCurrentSessionId,
listCachedPreviewSessions as listCachedSessions,
type SessionType,
type PreviewSessionMeta,
type CachedPreviewSessionInfo,
type CachedPreviewSessionEntry,
} from '@salesforce/agents';
Loading
Loading