Skip to content

Commit b0c2489

Browse files
Merge pull request #400 from salesforcecli/W-22203667/add-timestamp-sessiontype-to-preview-sessions
W-22203667: add timestamp, session type, and index to agent preview sessions output
2 parents c81a232 + cb042fd commit b0c2489

8 files changed

Lines changed: 338 additions & 707 deletions

File tree

messages/agent.preview.sessions.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ List all known programmatic agent preview sessions.
66

77
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.
88

9-
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.
9+
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.
1010

1111
# output.empty
1212

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

2121
Session ID
2222

23+
# output.tableHeader.timestamp
24+
25+
Started At
26+
27+
# output.tableHeader.sessionType
28+
29+
Session Type
30+
2331
# examples
2432

2533
- List all cached agent preview sessions:
2634

27-
<%= config.bin %> <%= command.id %>
35+
<%= config.bin %> <%= command.id %>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"@inquirer/prompts": "^7.10.1",
1313
"@oclif/core": "^4",
1414
"@oclif/multi-stage-output": "^0.8.36",
15-
"@salesforce/agents": "^1.1.2",
15+
"@salesforce/agents": "^1.2.0",
1616
"@salesforce/core": "^8.28.3",
1717
"@salesforce/kit": "^3.2.6",
1818
"@salesforce/sf-plugins-core": "^12.2.6",

schemas/agent-preview-sessions.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,21 @@
1515
},
1616
"sessionId": {
1717
"type": "string"
18+
},
19+
"timestamp": {
20+
"type": "string"
21+
},
22+
"sessionType": {
23+
"$ref": "#/definitions/SessionType"
1824
}
1925
},
2026
"required": ["agentId", "sessionId"],
2127
"additionalProperties": false
2228
}
29+
},
30+
"SessionType": {
31+
"type": "string",
32+
"enum": ["simulated", "live", "published"]
2333
}
2434
}
2535
}

src/commands/agent/preview/sessions.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@
1616

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

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

24-
export type AgentPreviewSessionsResult = Array<{ agentId: string; displayName?: string; sessionId: string }>;
24+
export type AgentPreviewSessionsResult = Array<{
25+
agentId: string;
26+
displayName?: string;
27+
sessionId: string;
28+
timestamp?: string;
29+
sessionType?: SessionType;
30+
}>;
2531

2632
export default class AgentPreviewSessions extends SfCommand<AgentPreviewSessionsResult> {
2733
public static readonly summary = messages.getMessage('summary');
@@ -36,9 +42,9 @@ export default class AgentPreviewSessions extends SfCommand<AgentPreviewSessions
3642
public async run(): Promise<AgentPreviewSessionsResult> {
3743
const entries = await listCachedSessions(this.project!);
3844
const rows: AgentPreviewSessionsResult = [];
39-
for (const { agentId, displayName, sessionIds } of entries) {
40-
for (const sessionId of sessionIds) {
41-
rows.push({ agentId, displayName, sessionId });
45+
for (const { agentId, displayName, sessions } of entries) {
46+
for (const { sessionId, timestamp, sessionType } of sessions) {
47+
rows.push({ agentId, displayName, sessionId, timestamp, sessionType });
4248
}
4349
}
4450

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

5460
const agentColumnHeader = messages.getMessage('output.tableHeader.agent');
5561
const sessionIdHeader = messages.getMessage('output.tableHeader.sessionId');
62+
const timestampHeader = messages.getMessage('output.tableHeader.timestamp');
63+
const sessionTypeHeader = messages.getMessage('output.tableHeader.sessionType');
5664
const tableData = rows.map((r) => ({
5765
agent: r.displayName ?? r.agentId,
5866
sessionId: r.sessionId,
67+
timestamp: r.timestamp ?? '',
68+
sessionType: r.sessionType ?? '',
5969
}));
6070
this.table({
6171
data: tableData,
6272
columns: [
6373
{ key: 'agent', name: agentColumnHeader },
6474
{ key: 'sessionId', name: sessionIdHeader },
75+
{ key: 'timestamp', name: timestampHeader },
76+
{ key: 'sessionType', name: sessionTypeHeader },
6577
],
6678
});
6779
return rows;

src/commands/agent/preview/start.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { Flags, SfCommand, toHelpSection } from '@salesforce/sf-plugins-core';
1818
import { EnvironmentVariable, Lifecycle, Messages, SfError } from '@salesforce/core';
1919
import { Agent, ProductionAgent, ScriptAgent } from '@salesforce/agents';
20-
import { createCache } from '../../../previewSessionStore.js';
20+
import { createCache, SessionType } from '../../../previewSessionStore.js';
2121
import { COMPILATION_API_EXIT_CODES } from '../../../common.js';
2222

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

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

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

163164
await Lifecycle.getInstance().emitTelemetry({ eventName: 'agent_preview_start_success' });
164165
const result: AgentPreviewStartResult = { sessionId: session.sessionId, agentApiName: agentIdentifier };
165166
this.log(messages.getMessage('output.sessionId', [session.sessionId]));
166167
return result;
167168
}
168169
}
170+
171+
function resolveSessionType(agent: ScriptAgent | ProductionAgent, simulateActions: boolean | undefined): SessionType {
172+
if (agent instanceof ProductionAgent) return 'published';
173+
return simulateActions ? 'simulated' : 'live';
174+
}

src/previewSessionStore.ts

Lines changed: 12 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -14,159 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { readdir, readFile, unlink, writeFile } from 'node:fs/promises';
18-
import { join } from 'node:path';
19-
import { SfError } from '@salesforce/core';
20-
import type { SfProject } from '@salesforce/core';
21-
import type { ProductionAgent, ScriptAgent } from '@salesforce/agents';
22-
23-
const SESSION_META_FILE = 'session-meta.json';
24-
25-
export type SessionMeta = { displayName?: string };
26-
27-
/**
28-
* Save a marker so send/end can validate that the session was started for this agent.
29-
* Caller must have started the session (agent has sessionId set). Uses agent.getHistoryDir() for the path.
30-
* Pass displayName (authoring bundle name or production agent API name) so "agent preview sessions" can show it.
31-
*/
32-
export async function createCache(
33-
agent: ScriptAgent | ProductionAgent,
34-
options?: { displayName?: string }
35-
): Promise<void> {
36-
const historyDir = await agent.getHistoryDir();
37-
const metaPath = join(historyDir, SESSION_META_FILE);
38-
const meta: SessionMeta = { displayName: options?.displayName };
39-
await writeFile(metaPath, JSON.stringify(meta), 'utf-8');
40-
}
41-
42-
/**
43-
* Validate that the session was started for this agent (marker file exists in agent's history dir for current sessionId).
44-
* Caller must set sessionId on the agent (agent.setSessionId) before calling.
45-
* Throws SfError if the session marker is not found.
46-
*/
47-
export async function validatePreviewSession(agent: ScriptAgent | ProductionAgent): Promise<void> {
48-
const historyDir = await agent.getHistoryDir();
49-
const metaPath = join(historyDir, SESSION_META_FILE);
50-
try {
51-
await readFile(metaPath, 'utf-8');
52-
} catch {
53-
throw new SfError(
54-
'No preview session found for this session ID. Run "sf agent preview start" first.',
55-
'PreviewSessionNotFound'
56-
);
57-
}
58-
}
59-
60-
/**
61-
* Remove the session marker so this session is no longer considered "active" for send/end without --session-id.
62-
* Call after ending the session. Caller must set sessionId on the agent before calling.
63-
*/
64-
export async function removeCache(agent: ScriptAgent | ProductionAgent): Promise<void> {
65-
const historyDir = await agent.getHistoryDir();
66-
const metaPath = join(historyDir, SESSION_META_FILE);
67-
try {
68-
await unlink(metaPath);
69-
} catch {
70-
// already removed or never created
71-
}
72-
}
73-
74-
/**
75-
* List session IDs that have a cache marker (started via "agent preview start") for this agent.
76-
* Uses project path and agent's storage ID to find .sfdx/agents/<agentId>/sessions/<sessionId>/session-meta.json.
77-
*/
78-
export async function getCachedSessionIds(project: SfProject, agent: ScriptAgent | ProductionAgent): Promise<string[]> {
79-
const agentId = agent.getAgentIdForStorage();
80-
const base = join(project.getPath(), '.sfdx');
81-
const sessionsDir = join(base, 'agents', agentId, 'sessions');
82-
const sessionIds: string[] = [];
83-
try {
84-
const entries = await readdir(sessionsDir, { withFileTypes: true });
85-
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
86-
const hasMarker = await Promise.all(
87-
dirs.map(async (name) => {
88-
try {
89-
await readFile(join(sessionsDir, name, SESSION_META_FILE), 'utf-8');
90-
return true;
91-
} catch {
92-
return false;
93-
}
94-
})
95-
);
96-
dirs.forEach((name, i) => {
97-
if (hasMarker[i]) sessionIds.push(name);
98-
});
99-
} catch {
100-
// sessions dir missing or unreadable
101-
}
102-
return sessionIds;
103-
}
104-
105-
/**
106-
* Return the single "current" session ID when safe: exactly one cached session for this agent.
107-
* Returns undefined when there are zero or multiple sessions (caller should require --session-id).
108-
*/
109-
export async function getCurrentSessionId(
110-
project: SfProject,
111-
agent: ScriptAgent | ProductionAgent
112-
): Promise<string | undefined> {
113-
const ids = await getCachedSessionIds(project, agent);
114-
return ids.length === 1 ? ids[0] : undefined;
115-
}
116-
117-
export type CachedSessionEntry = { agentId: string; displayName?: string; sessionIds: string[] };
118-
119-
/**
120-
* List all cached preview sessions in the project, grouped by agent ID.
121-
* displayName (when present in session-meta.json) is the authoring bundle name or production agent API name for display.
122-
* Use this to show users which sessions exist so they can end or clean up.
123-
*/
124-
export async function listCachedSessions(project: SfProject): Promise<CachedSessionEntry[]> {
125-
const base = join(project.getPath(), '.sfdx', 'agents');
126-
const result: CachedSessionEntry[] = [];
127-
try {
128-
const agentDirs = await readdir(base, { withFileTypes: true });
129-
const entries = await Promise.all(
130-
agentDirs
131-
.filter((ent) => ent.isDirectory())
132-
.map(async (ent) => {
133-
const agentId = ent.name;
134-
const sessionsDir = join(base, agentId, 'sessions');
135-
let sessionIds: string[] = [];
136-
let displayName: string | undefined;
137-
try {
138-
const sessionDirs = await readdir(sessionsDir, { withFileTypes: true });
139-
const withMarker = await Promise.all(
140-
sessionDirs
141-
.filter((s) => s.isDirectory())
142-
.map(async (s) => {
143-
try {
144-
await readFile(join(sessionsDir, s.name, SESSION_META_FILE), 'utf-8');
145-
return s.name;
146-
} catch {
147-
return null;
148-
}
149-
})
150-
);
151-
sessionIds = withMarker.filter((id): id is string => id !== null);
152-
if (sessionIds.length > 0) {
153-
try {
154-
const raw = await readFile(join(sessionsDir, sessionIds[0], SESSION_META_FILE), 'utf-8');
155-
const meta = JSON.parse(raw) as SessionMeta;
156-
displayName = meta.displayName;
157-
} catch {
158-
// ignore
159-
}
160-
}
161-
} catch {
162-
// no sessions dir or unreadable
163-
}
164-
return { agentId, displayName, sessionIds };
165-
})
166-
);
167-
result.push(...entries.filter((e) => e.sessionIds.length > 0));
168-
} catch {
169-
// no agents dir or unreadable
170-
}
171-
return result;
172-
}
17+
export {
18+
createPreviewSessionCache as createCache,
19+
validatePreviewSession,
20+
removePreviewSessionCache as removeCache,
21+
getCachedPreviewSessionIds as getCachedSessionIds,
22+
getCurrentPreviewSessionId as getCurrentSessionId,
23+
listCachedPreviewSessions as listCachedSessions,
24+
type SessionType,
25+
type PreviewSessionMeta,
26+
type CachedPreviewSessionInfo,
27+
type CachedPreviewSessionEntry,
28+
} from '@salesforce/agents';

0 commit comments

Comments
 (0)