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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ List recent agent conversations
**Flags:**
- `-n, --limit <value> - Number of conversations (1-1000) - (default: "25")`
- `-q, --query <value> - Any matching span selects its conversation; totals include all spans in selected projects and time range`
- `-s, --sort <value> - One conversation field; prefix - for descending (default: -conversation.age; ID breaks ties)`
- `-t, --period <value> - Time range: "7d", "2024-01-01..2024-02-01", ">=2024-01-01" - (default: "7d")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`
Expand Down Expand Up @@ -63,6 +64,12 @@ sentry agent-conversation list my-project
# Show more, last 24 hours
sentry agent-conversation list --limit 50 --period 24h

# Sort by highest error count
sentry agent-conversation list --sort -conversation.errors

# Sort by lowest total cost
sentry agent-conversation list --sort conversation.totalCost

# Find conversations with errors
sentry agent-conversation list -q "conversation.errors:>0"

Expand Down
50 changes: 47 additions & 3 deletions packages/cli/src/commands/agent-conversation/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { formatConversationTable } from "../../lib/formatters/conversation.js";
import { filterFields } from "../../lib/formatters/json.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import {
appendQueryHint,
appendSortHint,
buildListCommand,
LIST_DEFAULT_LIMIT,
LIST_MAX_LIMIT,
Expand All @@ -43,9 +45,31 @@ import {
ConversationListItemSchema,
} from "../../types/conversation.js";

const CONVERSATION_SORT_FIELDS = [
"conversation.age",
"conversation.timeSpan",
"conversation.generationDuration",
"conversation.errors",
"conversation.llmCalls",
"conversation.toolCalls",
"conversation.toolErrors",
"conversation.inputTokens",
"conversation.outputTokens",
"conversation.totalTokens",
"conversation.totalCost",
] as const;

type ConversationSortField = (typeof CONVERSATION_SORT_FIELDS)[number];
type ConversationSort = ConversationSortField | `-${ConversationSortField}`;

const CONVERSATION_SORT_VALUES = CONVERSATION_SORT_FIELDS.flatMap(
(field): ConversationSort[] => [field, `-${field}`],
);

type ListFlags = {
readonly limit: number;
readonly query?: string;
readonly sort?: ConversationSort;
readonly period: TimeRange;
readonly json: boolean;
readonly cursor?: string;
Expand Down Expand Up @@ -106,6 +130,9 @@ export const listCommand = buildListCommand("agent-conversation", {
brief: "List recent agent conversations",
fullDescription:
"List recent agent conversations from Sentry projects.\n\n" +
"Sort by one canonical conversation.* field. Prefix the field with - for descending order. " +
"The default is -conversation.age, and conversation ID ascending breaks ties. " +
"conversation.timeSpan measures elapsed time; conversation.generationDuration sums model call duration.\n\n" +
"Target patterns:\n" +
" sentry agent-conversation list # Auto-detect organization\n" +
" sentry agent-conversation list <org>/ # All projects in an organization\n" +
Expand Down Expand Up @@ -133,6 +160,14 @@ export const listCommand = buildListCommand("agent-conversation", {
description: "Show more, last 24 hours",
command: "sentry agent-conversation list --limit 50 --period 24h",
},
{
description: "Sort by highest error count",
command: "sentry agent-conversation list --sort -conversation.errors",
},
{
description: "Sort by lowest total cost",
command: "sentry agent-conversation list --sort conversation.totalCost",
},
{
description: "Find conversations with errors",
command: 'sentry agent-conversation list -q "conversation.errors:>0"',
Expand Down Expand Up @@ -179,12 +214,20 @@ export const listCommand = buildListCommand("agent-conversation", {
"Any matching span selects its conversation; totals include all spans in selected projects and time range",
optional: true,
},
sort: {
kind: "enum",
values: CONVERSATION_SORT_VALUES,
brief:
"One conversation field; prefix - for descending (default: -conversation.age; ID breaks ties)",
optional: true,
},
period: LIST_PERIOD_FLAG,
},
aliases: {
...PERIOD_ALIASES,
n: "limit",
q: "query",
s: "sort",
},
},
async *func(this: SentryContext, flags: ListFlags, target?: string) {
Expand All @@ -202,6 +245,7 @@ export const listCommand = buildListCommand("agent-conversation", {

const contextKey = buildPaginationContextKey("agent-conversation", scope, {
q: flags.query,
sort: flags.sort,
period: serializeTimeRange(flags.period),
});
const { cursor, direction } = resolveCursor(
Expand All @@ -220,6 +264,7 @@ export const listCommand = buildListCommand("agent-conversation", {
() =>
listConversations(org, {
query: flags.query,
sort: flags.sort,
limit: flags.limit,
cursor,
project: projectId === undefined ? undefined : String(projectId),
Expand All @@ -241,9 +286,8 @@ export const listCommand = buildListCommand("agent-conversation", {
});

const parts: string[] = [];
if (flags.query) {
parts.push(`-q "${flags.query}"`);
}
appendQueryHint(parts, flags.query);
appendSortHint(parts, flags.sort);
appendPeriodHint(parts, flags.period, DEFAULT_PERIOD);
const flagSuffix = parts.length > 0 ? ` ${parts.join(" ")}` : "";

Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/lib/api/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ async function fetchConversationsPage(
orgSlug: string,
options: {
query?: string;
sort?: string;
cursor?: string;
statsPeriod?: string;
start?: string;
Expand All @@ -74,6 +75,9 @@ async function fetchConversationsPage(
if (options.query) {
params.query = options.query;
}
if (options.sort) {
params.sort = options.sort;
}
if (options.project) {
params.project = options.project;
}
Expand All @@ -96,13 +100,14 @@ async function fetchConversationsPage(
* pages using cursor-based pagination (bounded by {@link MAX_PAGINATION_PAGES}).
*
* @param orgSlug - Organization slug
* @param options - Query options (query, limit, cursor, statsPeriod, etc.)
* @param options - Query options (query, sort, limit, cursor, statsPeriod, etc.)
* @returns Paginated response with conversation items and optional next cursor
*/
export async function listConversations(
orgSlug: string,
options: {
query?: string;
sort?: string;
limit?: number;
cursor?: string;
statsPeriod?: string;
Expand Down
38 changes: 34 additions & 4 deletions packages/cli/test/commands/agent-conversation/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* - Organization and project target resolution
* - Organization auto-detection
* - Yielding CommandOutput with conversation data
* - Query filter passthrough
* - Query and sort passthrough
* - Time params passthrough
* - Pagination hints with -q flag preserved
* - Pagination hints with active filters preserved
* - Empty result handling
*
* Uses spyOn mocking to avoid real HTTP calls or database access.
Expand Down Expand Up @@ -364,6 +364,26 @@ describe("listCommand.func", () => {
);
});

test("passes sort to API", async () => {
listConversationsSpy.mockResolvedValue({
data: [],
nextCursor: undefined,
});

const { context } = createMockContext();
const func = await listCommand.loader();
await func.call(
context,
{ ...JSON_FLAGS, sort: "-conversation.totalCost" },
ORG,
);

expect(listConversationsSpy).toHaveBeenCalledWith(
ORG,
expect.objectContaining({ sort: "-conversation.totalCost" }),
);
});

test("passes time params to API", async () => {
listConversationsSpy.mockResolvedValue({
data: [],
Expand Down Expand Up @@ -400,7 +420,7 @@ describe("listCommand.func", () => {
);
});

test("preserves project and query in pagination hints", async () => {
test("preserves project, query, and sort in pagination hints", async () => {
resolveTargetSpy.mockResolvedValue({
org: ORG,
project: PROJECT.slug,
Expand All @@ -415,7 +435,11 @@ describe("listCommand.func", () => {
const func = await listCommand.loader();
await func.call(
context,
{ ...HUMAN_FLAGS, query: "conversation.errors:>0" },
{
...HUMAN_FLAGS,
query: "conversation.errors:>0",
sort: "-conversation.totalCost",
},
`${ORG}/${PROJECT.slug}`,
);

Expand All @@ -425,6 +449,12 @@ describe("listCommand.func", () => {
`agent-conversation list ${ORG}/${PROJECT.slug} -c next`,
);
expect(output).toContain('-q "conversation.errors:>0"');
expect(output).toContain('--sort "-conversation.totalCost"');
expect(resolveCursorSpy).toHaveBeenCalledWith(
undefined,
"agent-conversation-list",
expect.stringContaining("sort:-conversation.totalCost"),
);
});

test("handles empty results (human mode)", async () => {
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/test/lib/api/conversations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ describe("listConversations", () => {
expect(decodeURIComponent(getCapturedUrl())).toContain("query=has:errors");
});

test("passes sort param", async () => {
const { getCapturedUrl } = mockOk([]);

await listConversations(ORG, { sort: "-conversation.totalCost" });

expect(getCapturedUrl()).toContain("sort=-conversation.totalCost");
});

test("passes statsPeriod param", async () => {
const { getCapturedUrl } = mockOk([]);

Expand Down Expand Up @@ -187,6 +195,7 @@ describe("listConversations", () => {

const url = getCapturedUrl();
expect(url).not.toContain("query=");
expect(url).not.toContain("sort=");
expect(url).not.toContain("statsPeriod=");
expect(url).not.toContain("start=");
expect(url).not.toContain("end=");
Expand Down
Loading