diff --git a/apps/cli/src/runs/repo.ts b/apps/cli/src/runs/repo.ts index ed87ebae..5b741f2b 100644 --- a/apps/cli/src/runs/repo.ts +++ b/apps/cli/src/runs/repo.ts @@ -22,6 +22,7 @@ export const CreateRunOptions = Run.pick({ export interface IRunsRepo { create(options: z.infer): Promise>; fetch(id: string): Promise>; + list(cursor?: string): Promise>; appendEvents(runId: string, events: z.infer[]): Promise; } @@ -76,4 +77,68 @@ export class FSRunsRepo implements IRunsRepo { log: events, }; } + + async list(cursor?: string): Promise> { + const runsDir = path.join(WorkDir, 'runs'); + const PAGE_SIZE = 20; + + let files: string[] = []; + try { + const entries = await fsp.readdir(runsDir, { withFileTypes: true }); + files = entries + .filter(e => e.isFile() && e.name.endsWith('.jsonl')) + .map(e => e.name); + } catch (err: any) { + if (err && err.code === 'ENOENT') { + return { runs: [] }; + } + throw err; + } + + files.sort((a, b) => b.localeCompare(a)); + + const cursorFile = cursor; + let startIndex = 0; + if (cursorFile) { + const exact = files.indexOf(cursorFile); + if (exact >= 0) { + startIndex = exact + 1; + } else { + const firstOlder = files.findIndex(name => name.localeCompare(cursorFile) < 0); + startIndex = firstOlder === -1 ? files.length : firstOlder; + } + } + + const selected = files.slice(startIndex, startIndex + PAGE_SIZE); + const runs: z.infer['runs'] = []; + + for (const name of selected) { + const runId = name.slice(0, -'.jsonl'.length); + try { + const contents = await fsp.readFile(path.join(runsDir, name), 'utf8'); + const firstLine = contents.split('\n').find(line => line.trim() !== ''); + if (!firstLine) { + continue; + } + const start = StartEvent.parse(JSON.parse(firstLine)); + runs.push({ + id: runId, + createdAt: start.ts!, + agentId: start.agentName, + }); + } catch { + continue; + } + } + + const hasMore = startIndex + PAGE_SIZE < files.length; + const nextCursor = hasMore && selected.length > 0 + ? selected[selected.length - 1] + : undefined; + + return { + runs, + ...(nextCursor ? { nextCursor } : {}), + }; + } } \ No newline at end of file diff --git a/apps/cli/src/server.ts b/apps/cli/src/server.ts index b56f7331..fde66419 100644 --- a/apps/cli/src/server.ts +++ b/apps/cli/src/server.ts @@ -12,9 +12,8 @@ import { ModelConfig, Provider } from "./models/models.js"; import { IAgentsRepo } from "./agents/repo.js"; import { Agent } from "./agents/agents.js"; import { AskHumanResponsePayload, authorizePermission, createMessage, createRun, replyToHumanInputRequest, Run, stop, ToolPermissionAuthorizePayload } from './runs/runs.js'; -import { IRunsRepo, ListRunsResponse, CreateRunOptions } from './runs/repo.js'; +import { IRunsRepo, CreateRunOptions, ListRunsResponse } from './runs/repo.js'; import { IBus } from './application/lib/bus.js'; -import { RunEvent } from './entities/run-events.js'; let id = 0; @@ -462,6 +461,31 @@ const routes = new Hono() return c.json(run); } ) + .get( + '/runs', + describeRoute({ + summary: 'List runs', + description: 'List all runs', + responses: { + 200: { + description: 'Runs list', + content: { + 'application/json': { + schema: resolver(ListRunsResponse), + }, + }, + }, + }, + }), + validator('query', z.object({ + cursor: z.string().optional(), + })), + async (c) => { + const repo = container.resolve('runsRepo'); + const runs = await repo.list(c.req.valid('query').cursor); + return c.json(runs); + } + ) .post( '/runs/:runId/messages/new', describeRoute({