mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
fix(cli): list every agent in paseo agent ls, past the first 200 (#6033)
* fix(cli): list every agent page in agent ls agent ls made one fetch_agents call and ignored pageInfo.nextCursor, so it stopped at the daemon's default page of 200 agents without saying more existed. Follow the cursor until the daemon reports no next page, as workspace ls and agent delete already do. Co-authored-by: HTeo <hteo1337@gmail.com> * fix(cli): page agent ls in creation order fetch_agents defaults to updated_at order, so an agent that updated after the first page moved ahead of the cursor and was skipped. Page by created_at, which never changes; ls sorts the rows itself afterwards. * test(cli): exercise agent listing with a supplied client --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: HTeo <hteo1337@gmail.com>
This commit is contained in:
@@ -1,5 +1,60 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAgentLsFetchOptions } from "./ls.js";
|
||||
import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages";
|
||||
import { buildAgentLsFetchOptions, listAgents } from "./ls.js";
|
||||
|
||||
const daemonTarget = { kind: "endpoint" as const, host: "example.test:12345" };
|
||||
|
||||
function createPagingClient() {
|
||||
const daemonAgents: AgentSnapshotPayload[] = [3, 2, 1].map((n) => ({
|
||||
id: `${n}${n}${n}${n}${n}${n}${n}${n}-0000-4000-8000-000000000000`,
|
||||
provider: "codex",
|
||||
title: `agent ${n}`,
|
||||
status: "closed",
|
||||
archivedAt: null,
|
||||
cwd: "/tmp/project",
|
||||
createdAt: `2026-10-0${n}T00:00:00.000Z`,
|
||||
updatedAt: `2026-10-0${n}T00:00:00.000Z`,
|
||||
labels: {},
|
||||
model: null,
|
||||
lastUserMessageAt: null,
|
||||
currentModeId: null,
|
||||
availableModes: [],
|
||||
pendingPermissions: [],
|
||||
persistence: null,
|
||||
capabilities: {
|
||||
supportsStreaming: true,
|
||||
supportsSessionPersistence: true,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: true,
|
||||
supportsReasoningStream: true,
|
||||
supportsToolInvocations: true,
|
||||
},
|
||||
}));
|
||||
|
||||
// The fake daemon pages two agents at a time, newest first by the requested key
|
||||
// (updated_at by default), and agent 1 is updated right after the first page is served.
|
||||
const fetchAgents = async (options?: {
|
||||
sort?: { key: string }[];
|
||||
page?: { cursor?: string };
|
||||
}) => {
|
||||
const key: "createdAt" | "updatedAt" =
|
||||
options?.sort?.[0]?.key === "created_at" ? "createdAt" : "updatedAt";
|
||||
const cursor = options?.page?.cursor;
|
||||
const ordered = daemonAgents
|
||||
.filter((agent) => cursor === undefined || agent[key] < cursor)
|
||||
.sort((left, right) => right[key].localeCompare(left[key]));
|
||||
const page = ordered.slice(0, 2);
|
||||
const hasMore = ordered.length > 2;
|
||||
if (cursor === undefined) {
|
||||
daemonAgents[2]!.updatedAt = "2026-10-09T00:00:00.000Z";
|
||||
}
|
||||
return {
|
||||
entries: page.map((agent) => ({ agent })),
|
||||
pageInfo: { nextCursor: hasMore ? page[page.length - 1]![key] : null },
|
||||
};
|
||||
};
|
||||
return { fetchAgents, close: async () => {} };
|
||||
}
|
||||
|
||||
describe("buildAgentLsFetchOptions", () => {
|
||||
it("fetches active agents by default", () => {
|
||||
@@ -66,3 +121,13 @@ describe("buildAgentLsFetchOptions", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("runLsCommand", () => {
|
||||
it("lists agents on every page, including one updated during the listing", async () => {
|
||||
const result = await listAgents({ daemonTarget, all: true, global: true }, async () =>
|
||||
createPagingClient(),
|
||||
);
|
||||
|
||||
expect(result.data.map((item) => item.name)).toEqual(["agent 3", "agent 2", "agent 1"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -165,6 +165,14 @@ export function buildAgentLsFetchOptions(
|
||||
return fetchOptions;
|
||||
}
|
||||
|
||||
interface AgentLsClient {
|
||||
fetchAgents(options?: FetchAgentsOptions): Promise<{
|
||||
entries: { agent: AgentSnapshotPayload }[];
|
||||
pageInfo: { nextCursor: string | null };
|
||||
}>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent ls command semantics:
|
||||
* - `paseo agent ls` → active non-archived agents
|
||||
@@ -176,7 +184,16 @@ export async function runLsCommand(
|
||||
options: AgentLsOptions,
|
||||
_command: Command,
|
||||
): Promise<AgentLsResult> {
|
||||
const client = await connectToDaemon({ target: options.daemonTarget });
|
||||
return listAgents(options);
|
||||
}
|
||||
|
||||
export async function listAgents(
|
||||
options: AgentLsOptions,
|
||||
connect: (
|
||||
options: Parameters<typeof connectToDaemon>[0],
|
||||
) => Promise<AgentLsClient> = connectToDaemon,
|
||||
): Promise<AgentLsResult> {
|
||||
const client = await connect({ target: options.daemonTarget });
|
||||
|
||||
try {
|
||||
const normalizedThinkingOptionId = options.thinking?.trim();
|
||||
@@ -189,8 +206,19 @@ export async function runLsCommand(
|
||||
}
|
||||
|
||||
const labelFilters = parseLabelFilters(options.label);
|
||||
const fetchPayload = await client.fetchAgents(buildAgentLsFetchOptions(options));
|
||||
let agents = fetchPayload.entries.map((entry) => entry.agent);
|
||||
const fetchOptions = buildAgentLsFetchOptions(options);
|
||||
let agents: AgentSnapshotPayload[] = [];
|
||||
let cursor: string | undefined;
|
||||
// createdAt never changes, so agents cannot move past the cursor while the pages are read.
|
||||
do {
|
||||
const payload = await client.fetchAgents({
|
||||
...fetchOptions,
|
||||
sort: [{ key: "created_at", direction: "desc" }],
|
||||
page: { limit: 200, ...(cursor ? { cursor } : {}) },
|
||||
});
|
||||
agents.push(...payload.entries.map((entry) => entry.agent));
|
||||
cursor = payload.pageInfo.nextCursor ?? undefined;
|
||||
} while (cursor);
|
||||
|
||||
// By default, exclude archived agents. `-a` includes them.
|
||||
if (!options.all) {
|
||||
|
||||
Reference in New Issue
Block a user