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
58 changes: 52 additions & 6 deletions packages/cli/src/lib/api/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,37 @@ import { logger } from "../logger.js";
import { resolveOrgRegion } from "../region.js";

import {
API_MAX_PER_PAGE,
apiRequestToRegion,
autoPaginate,
MAX_PAGINATION_PAGES,
type PaginatedResponse,
parseLinkHeader,
} from "./infrastructure.js";

const log = logger.withTag("api.conversations");

export async function listConversations(
/**
* Fetch a single page of conversations from the AI-conversations endpoint.
*
* Internal helper used by {@link listConversations} for both single-page and
* multi-page (auto-paginating) fetches.
*/
async function fetchConversationsPage(
regionUrl: string,
orgSlug: string,
options: {
query?: string;
limit?: number;
cursor?: string;
statsPeriod?: string;
start?: string;
end?: string;
project?: string;
} = {}
},
perPage: number
): Promise<PaginatedResponse<ConversationListItem[]>> {
const regionUrl = await resolveOrgRegion(orgSlug);

const params: Record<string, string> = {
per_page: String(options.limit ?? 10),
per_page: String(perPage),
};
if (options.statsPeriod) {
params.statsPeriod = options.statsPeriod;
Expand Down Expand Up @@ -81,6 +88,45 @@ export async function listConversations(
return { data, nextCursor };
}

/**
* List AI conversations for an organization.
*
* When `limit` exceeds {@link API_MAX_PER_PAGE}, transparently fetches multiple
* pages using cursor-based pagination (bounded by {@link MAX_PAGINATION_PAGES}).
*
* @param orgSlug - Organization slug
* @param options - Query options (query, limit, cursor, statsPeriod, etc.)
* @returns Paginated response with conversation items and optional next cursor
*/
export async function listConversations(
orgSlug: string,
options: {
query?: string;
limit?: number;
cursor?: string;
statsPeriod?: string;
start?: string;
end?: string;
project?: string;
} = {}
): Promise<PaginatedResponse<ConversationListItem[]>> {
const regionUrl = await resolveOrgRegion(orgSlug);
const limit = options.limit ?? 10;
const perPage = Math.min(limit, API_MAX_PER_PAGE);

return autoPaginate(
(cursor) =>
fetchConversationsPage(
regionUrl,
orgSlug,
{ ...options, cursor },
perPage
),
limit,
options.cursor
);
}

export async function getConversationSpans(
orgSlug: string,
conversationId: string,
Expand Down
40 changes: 40 additions & 0 deletions packages/cli/test/lib/api/conversations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,46 @@ describe("listConversations", () => {
expect(result.data).toHaveLength(1);
expect(result.data[0].conversationId).toBe("conv-abc");
});

test("caps per_page at API_MAX_PER_PAGE when limit exceeds it", async () => {
const { getCapturedUrl } = mockOk([]);

await listConversations(ORG, { limit: 200 });

expect(getCapturedUrl()).toContain("per_page=100");
expect(getCapturedUrl()).not.toContain("per_page=200");
});

test("accumulates results across pages when limit > API_MAX_PER_PAGE", async () => {
const item = (id: string) => ({
conversationId: id,
flow: [],
errors: 0,
llmCalls: 0,
toolCalls: 0,
totalTokens: 0,
totalCost: 0,
startTimestamp: 1_716_500_000,
endTimestamp: 1_716_500_060,
traceCount: 0,
traceIds: [],
firstInput: "",
lastOutput: "",
toolNames: [],
toolErrors: 0,
});

const { getCapturedUrls } = mockSequential([
{ body: [item("c1")], headers: linkHeader("cursor1") },
{ body: [item("c2")], headers: linkHeader("cursor2", "false") },
]);

const result = await listConversations(ORG, { limit: 200 });

expect(result.data).toHaveLength(2);
expect(result.data.map((c) => c.conversationId)).toEqual(["c1", "c2"]);
expect(getCapturedUrls()[0]).toContain("per_page=100");
});
});

// ============================================================================
Expand Down
Loading