Skip to content
Open
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 command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,14 @@
],
"plugin": "@salesforce/plugin-agent"
},
{
"alias": [],
"command": "agent:trace:delete",
"flagAliases": [],
"flagChars": ["a"],
"flags": ["agent", "flags-dir", "json", "no-prompt", "older-than", "session-id"],
"plugin": "@salesforce/plugin-agent"
},
{
"alias": [],
"command": "agent:trace:list",
Expand Down
87 changes: 87 additions & 0 deletions messages/agent.trace.delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# summary

Delete agent preview trace files.

# description

Deletes trace files recorded during agent preview sessions. By default, shows a preview of what will be deleted and prompts for confirmation. Use --no-prompt to skip confirmation.

Without filters, deletes all traces for all agents and sessions. Use flags to narrow the scope: filter by agent name (--agent), by session (--session-id), or by age (--older-than).

# flags.agent.summary

Only delete traces for this agent name (substring match). Matches against the name used when starting the session, whether that's an authoring bundle or a published agent API name.

# flags.session-id.summary

Only delete traces from this session ID.

# flags.older-than.summary

Only delete traces older than this duration. Accepts a number followed by a unit: m/minutes, h/hours, d/days, w/weeks (e.g. 7d, 24h, 2w).

# flags.no-prompt.summary

Skip the confirmation prompt and delete immediately.

# error.invalidOlderThan

Invalid --older-than value: '%s'. Use a number followed by a unit: m/minutes, h/hours, d/days, w/weeks (e.g. 7d, 24h, 30m, 2w).

# prompt.confirm

Delete %s trace file(s)? This cannot be undone.

# output.noneFound

No trace files matched the specified filters.

# output.preview

Found %s trace file(s) to delete:

# output.cancelled

Deletion cancelled.

# output.deleted

Deleted %s trace file(s).

# output.tableHeader.agent

Agent

# output.tableHeader.sessionId

Session ID

# output.tableHeader.planId

Plan ID

# examples

- Delete all traces for all agents and sessions (with confirmation prompt):

<%= config.bin %> <%= command.id %>

- Delete all traces for a specific agent:

<%= config.bin %> <%= command.id %> --agent My_Agent

- Delete traces from a specific session:

<%= config.bin %> <%= command.id %> --session-id <SESSION_ID>

- Delete traces older than 7 days:

<%= config.bin %> <%= command.id %> --older-than 7d

- Delete traces older than 24 hours for a specific agent, no prompt:

<%= config.bin %> <%= command.id %> --agent My_Agent --older-than 24h --no-prompt

- Delete all traces without confirmation:

<%= config.bin %> <%= command.id %> --no-prompt
28 changes: 28 additions & 0 deletions schemas/agent-trace-delete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/AgentTraceDeleteResult",
"definitions": {
"AgentTraceDeleteResult": {
"type": "array",
"items": {
"type": "object",
"properties": {
"agent": {
"type": "string"
},
"sessionId": {
"type": "string"
},
"planId": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": ["agent", "sessionId", "planId", "path"],
"additionalProperties": false
}
}
}
}
141 changes: 141 additions & 0 deletions src/commands/agent/trace/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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.
*/

import { unlink } from 'node:fs/promises';
import { Flags, SfCommand, toHelpSection } from '@salesforce/sf-plugins-core';
import { Messages, SfError } from '@salesforce/core';
import { listCachedPreviewSessions, listSessionTraces, type TraceFileInfo } from '@salesforce/agents';
import yesNoOrCancel from '../../../yes-no-cancel.js';

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

const DURATION_RE = /^(\d+)(d|h|m|w|days?|hours?|minutes?|weeks?)$/i;
const UNIT_MS: Record<string, number> = {
m: 60_000,
minute: 60_000,
minutes: 60_000,
h: 3_600_000,
hour: 3_600_000,
hours: 3_600_000,
d: 86_400_000,
day: 86_400_000,
days: 86_400_000,
w: 604_800_000,
week: 604_800_000,
weeks: 604_800_000,
};

export type AgentTraceDeleteResult = Array<{
agent: string;
sessionId: string;
planId: string;
path: string;
}>;

export default class AgentTraceDelete extends SfCommand<AgentTraceDeleteResult> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');
public static readonly requiresProject = true;

public static readonly errorCodes = toHelpSection('ERROR CODES', {
'Succeeded (0)': 'Traces deleted successfully (or no traces matched).',
});

public static readonly flags = {
agent: Flags.string({
summary: messages.getMessage('flags.agent.summary'),
char: 'a',
}),
'session-id': Flags.string({
summary: messages.getMessage('flags.session-id.summary'),
}),
'older-than': Flags.custom<Date>({
summary: messages.getMessage('flags.older-than.summary'),
// eslint-disable-next-line @typescript-eslint/require-await
parse: async (raw): Promise<Date> => {
const match = DURATION_RE.exec(raw);
if (!match) {
throw new SfError(messages.getMessage('error.invalidOlderThan', [raw]), 'InvalidDuration');
}
const ms = parseInt(match[1], 10) * UNIT_MS[match[2].toLowerCase()];
return new Date(Date.now() - ms);
},
})(),
'no-prompt': Flags.boolean({
summary: messages.getMessage('flags.no-prompt.summary'),
}),
};

public async run(): Promise<AgentTraceDeleteResult> {
const { flags } = await this.parse(AgentTraceDelete);

const agentFilter = flags.agent?.toLowerCase();
const cachedAgents = await listCachedPreviewSessions(this.project!);

const candidates: AgentTraceDeleteResult = [];
for (const { agentId, displayName, sessions } of cachedAgents) {
if (agentFilter && !displayName?.toLowerCase().includes(agentFilter)) continue;

for (const { sessionId } of sessions) {
if (flags['session-id'] && sessionId !== flags['session-id']) continue;

// eslint-disable-next-line no-await-in-loop
let traces: TraceFileInfo[] = await listSessionTraces(agentId, sessionId);

if (flags['older-than']) {
traces = traces.filter((t) => t.mtime < flags['older-than']!);
}

for (const t of traces) {
candidates.push({ agent: displayName ?? agentId, sessionId, planId: t.planId, path: t.path });
}
}
}

if (candidates.length === 0) {
this.log(messages.getMessage('output.noneFound'));
return [];
}

if (!flags['no-prompt']) {
this.log(messages.getMessage('output.preview', [candidates.length]));
this.table({
data: candidates.map((c) => ({ agent: c.agent, sessionId: c.sessionId, planId: c.planId })),
columns: [
{ key: 'agent', name: messages.getMessage('output.tableHeader.agent') },
{ key: 'sessionId', name: messages.getMessage('output.tableHeader.sessionId') },
{ key: 'planId', name: messages.getMessage('output.tableHeader.planId') },
],
});

const confirmed = await yesNoOrCancel({
message: messages.getMessage('prompt.confirm', [candidates.length]),
default: false,
});

if (confirmed === 'cancel' || confirmed === false) {
this.log(messages.getMessage('output.cancelled'));
return [];
}
}

await Promise.all(candidates.map((c) => unlink(c.path)));
this.log(messages.getMessage('output.deleted', [candidates.length]));
return candidates;
}
}
Loading
Loading