diff --git a/docs/content/references/cli.mdx b/docs/content/references/cli.mdx index 73a5842e..f91146f4 100644 --- a/docs/content/references/cli.mdx +++ b/docs/content/references/cli.mdx @@ -67,7 +67,7 @@ Providers: `anthropic`, `google`, `openai`, `ollama`, `azure-openai`, `amazon-be | `--job-id ` | Custom Job ID for new Job (default: auto-generated) | | `--continue` | Continue latest Job with new Run | | `--continue-job ` | Continue specific Job with new Run | -| `--resume-from ` | Resume from specific checkpoint (requires `--continue` or `--continue-job`) | +| `--resume-from ` | Resume from specific checkpoint (requires `--continue-job`) | **Combining options:** @@ -75,9 +75,6 @@ Providers: `anthropic`, `google`, `openai`, `ollama`, `azure-openai`, `amazon-be # Continue latest Job from its latest checkpoint --continue -# Continue latest Job from a specific checkpoint ---continue --resume-from - # Continue specific Job from its latest checkpoint --continue-job @@ -85,7 +82,7 @@ Providers: `anthropic`, `google`, `openai`, `ollama`, `azure-openai`, `amazon-be --continue-job --resume-from ``` -**Note:** `--resume-from` must be combined with `--continue` or `--continue-job`. You can only resume from the Coordinator Expert's checkpoints. +**Note:** `--resume-from` requires `--continue-job` (Job ID must be specified). You can only resume from the Coordinator Expert's checkpoints. ### Interactive diff --git a/docs/content/using-experts/state-management.mdx b/docs/content/using-experts/state-management.mdx index 5116d5f1..f31b77b1 100644 --- a/docs/content/using-experts/state-management.mdx +++ b/docs/content/using-experts/state-management.mdx @@ -77,13 +77,9 @@ npx perstack run my-expert "Follow-up" --continue-job ## Resuming from a checkpoint -Resume from a specific checkpoint to branch execution. Use `--resume-from` with `--continue` or `--continue-job`: +Resume from a specific checkpoint to branch execution. Use `--resume-from` with `--continue-job`: ```bash -# Resume from checkpoint in latest Job -npx perstack run my-expert "try again" --continue --resume-from - -# Resume from checkpoint in specific Job npx perstack run my-expert "try again" --continue-job --resume-from ``` @@ -93,7 +89,7 @@ Useful for: - Debugging **Important:** -- `--resume-from` requires `--continue` or `--continue-job` +- `--resume-from` requires `--continue-job` (Job ID must be specified) - You can only resume from the Coordinator Expert's checkpoints ## Interactive tool calls diff --git a/e2e/run.test.ts b/e2e/run.test.ts index 532efa9d..e9aa1706 100644 --- a/e2e/run.test.ts +++ b/e2e/run.test.ts @@ -31,6 +31,6 @@ describe("CLI run", () => { "checkpoint-123", ]) expect(result.exitCode).toBe(1) - expect(result.stderr).toContain("--resume-from requires --continue or --continue-job") + expect(result.stderr).toContain("--resume-from requires --continue-job") }) }) diff --git a/packages/perstack/src/lib/context.ts b/packages/perstack/src/lib/context.ts index 6dc0bd0c..901dee37 100644 --- a/packages/perstack/src/lib/context.ts +++ b/packages/perstack/src/lib/context.ts @@ -2,7 +2,7 @@ import type { Checkpoint, PerstackConfig, ProviderConfig, ProviderName } from "@ import { getEnv } from "./get-env.js" import { getPerstackConfig } from "./perstack-toml.js" import { getProviderConfig } from "./provider-config.js" -import { getCheckpointById, getMostRecentCheckpoint, getMostRecentRunInJob } from "./run-manager.js" +import { getCheckpointById, getMostRecentCheckpoint } from "./run-manager.js" const defaultProvider: ProviderName = "anthropic" const defaultModel = "claude-sonnet-4-5" @@ -33,15 +33,12 @@ export async function resolveRunContext(input: ResolveRunContextInput): Promise< const perstackConfig = await getPerstackConfig(input.configPath) let checkpoint: Checkpoint | undefined if (input.resumeFrom) { - if (!input.continue && !input.continueJob) { - throw new Error("--resume-from requires --continue or --continue-job") + if (!input.continueJob) { + throw new Error("--resume-from requires --continue-job") } - const jobId = input.continueJob ?? (await getMostRecentCheckpoint()).jobId - const run = await getMostRecentRunInJob(jobId) - checkpoint = await getCheckpointById(jobId, run.runId, input.resumeFrom) + checkpoint = await getCheckpointById(input.continueJob, input.resumeFrom) } else if (input.continueJob) { - const run = await getMostRecentRunInJob(input.continueJob) - checkpoint = await getMostRecentCheckpoint(run.jobId, run.runId) + checkpoint = await getMostRecentCheckpoint(input.continueJob) } else if (input.continue) { checkpoint = await getMostRecentCheckpoint() } diff --git a/packages/perstack/src/lib/run-manager.ts b/packages/perstack/src/lib/run-manager.ts index 77b6ff3c..6f6355a4 100644 --- a/packages/perstack/src/lib/run-manager.ts +++ b/packages/perstack/src/lib/run-manager.ts @@ -1,8 +1,43 @@ import { existsSync } from "node:fs" import { readdir, readFile } from "node:fs/promises" import path from "node:path" -import { type Checkpoint, checkpointSchema, type RunEvent, type RunSetting } from "@perstack/core" -import { getRunDir } from "@perstack/runtime" +import { + type Checkpoint, + checkpointSchema, + type Job, + jobSchema, + type RunEvent, + type RunSetting, +} from "@perstack/core" +import { getCheckpointDir, getCheckpointPath, getRunDir } from "@perstack/runtime" + +export async function getAllJobs(): Promise { + const dataDir = path.resolve(process.cwd(), "perstack") + if (!existsSync(dataDir)) { + return [] + } + const jobsDir = path.resolve(dataDir, "jobs") + if (!existsSync(jobsDir)) { + return [] + } + const jobDirs = await readdir(jobsDir, { withFileTypes: true }).then((dirs) => + dirs.filter((dir) => dir.isDirectory()).map((dir) => dir.name), + ) + if (jobDirs.length === 0) { + return [] + } + const jobs: Job[] = [] + for (const jobDir of jobDirs) { + const jobPath = path.resolve(jobsDir, jobDir, "job.json") + try { + const jobContent = await readFile(jobPath, "utf-8") + jobs.push(jobSchema.parse(JSON.parse(jobContent))) + } catch { + // Ignore invalid jobs + } + } + return jobs.sort((a, b) => b.startedAt - a.startedAt) +} export async function getAllRuns(): Promise { const dataDir = path.resolve(process.cwd(), "perstack") @@ -67,63 +102,30 @@ export async function getMostRecentRunInJob(jobId: string): Promise return runs[0] } -export async function getCheckpoints( - jobId: string, - runId: string, -): Promise<{ timestamp: string; stepNumber: string; id: string }[]> { - const runDir = getRunDir(jobId, runId) - if (!existsSync(runDir)) { +export async function getCheckpointsByJobId(jobId: string): Promise { + const checkpointDir = getCheckpointDir(jobId) + if (!existsSync(checkpointDir)) { return [] } - return await readdir(runDir).then((files) => - files - .filter((file) => file.startsWith("checkpoint-")) - .map((file) => { - const [_, timestamp, stepNumber, id] = file.split(".")[0].split("-") - return { - timestamp, - stepNumber, - id, - } - }) - .sort((a, b) => Number(a.stepNumber) - Number(b.stepNumber)), - ) -} - -export async function getCheckpoint(checkpointId: string): Promise { - const run = await getMostRecentRun() - const runDir = getRunDir(run.jobId, run.runId) - const checkpointPath = path.resolve(runDir, `checkpoint-${checkpointId}.json`) - const checkpoint = await readFile(checkpointPath, "utf-8") - return checkpointSchema.parse(JSON.parse(checkpoint)) -} - -export async function getMostRecentCheckpoint(jobId?: string, runId?: string): Promise { - let runJobId: string - let runIdForCheckpoint: string - if (jobId && runId) { - runJobId = jobId - runIdForCheckpoint = runId - } else { - const run = await getMostRecentRun() - runJobId = run.jobId - runIdForCheckpoint = run.runId - } - const runDir = getRunDir(runJobId, runIdForCheckpoint) - const checkpointFiles = await readdir(runDir, { withFileTypes: true }).then((files) => - files.filter((file) => file.isFile() && file.name.startsWith("checkpoint-")), - ) - if (checkpointFiles.length === 0) { - throw new Error(`No checkpoints found for run ${runIdForCheckpoint}`) - } + const files = await readdir(checkpointDir) + const checkpointFiles = files.filter((file) => file.endsWith(".json")) const checkpoints = await Promise.all( checkpointFiles.map(async (file) => { - const checkpointPath = path.resolve(runDir, file.name) - const checkpoint = await readFile(checkpointPath, "utf-8") - return checkpointSchema.parse(JSON.parse(checkpoint)) + const checkpointPath = path.resolve(checkpointDir, file) + const content = await readFile(checkpointPath, "utf-8") + return checkpointSchema.parse(JSON.parse(content)) }), ) - return checkpoints.sort((a, b) => b.stepNumber - a.stepNumber)[0] + return checkpoints.sort((a, b) => a.stepNumber - b.stepNumber) +} + +export async function getMostRecentCheckpoint(jobId?: string): Promise { + const targetJobId = jobId ?? (await getMostRecentRun()).jobId + const checkpoints = await getCheckpointsByJobId(targetJobId) + if (checkpoints.length === 0) { + throw new Error(`No checkpoints found for job ${targetJobId}`) + } + return checkpoints[checkpoints.length - 1] } export async function getRecentExperts( @@ -168,51 +170,26 @@ export async function getEvents( .sort((a, b) => Number(a.stepNumber) - Number(b.stepNumber)), ) } -export async function getCheckpointById( - jobId: string, - runId: string, - checkpointId: string, -): Promise { - const runDir = getRunDir(jobId, runId) - const files = await readdir(runDir) - const checkpointFile = files.find( - (file) => file.startsWith("checkpoint-") && file.includes(`-${checkpointId}.`), - ) - if (!checkpointFile) { - throw new Error(`Checkpoint ${checkpointId} not found in run ${runId}`) +export async function getCheckpointById(jobId: string, checkpointId: string): Promise { + const checkpointPath = getCheckpointPath(jobId, checkpointId) + if (!existsSync(checkpointPath)) { + throw new Error(`Checkpoint ${checkpointId} not found in job ${jobId}`) } - const checkpointPath = path.resolve(runDir, checkpointFile) const checkpoint = await readFile(checkpointPath, "utf-8") return checkpointSchema.parse(JSON.parse(checkpoint)) } export async function getCheckpointsWithDetails( jobId: string, - runId: string, -): Promise< - { id: string; runId: string; stepNumber: number; timestamp: number; contextWindowUsage: number }[] -> { - const runDir = getRunDir(jobId, runId) - if (!existsSync(runDir)) { - return [] - } - const files = await readdir(runDir) - const checkpointFiles = files.filter((file) => file.startsWith("checkpoint-")) - const checkpoints = await Promise.all( - checkpointFiles.map(async (file) => { - const [_, timestamp, stepNumber, id] = file.split(".")[0].split("-") - const checkpointPath = path.resolve(runDir, file) - const content = await readFile(checkpointPath, "utf-8") - const checkpoint = checkpointSchema.parse(JSON.parse(content)) - return { - id, - runId, - stepNumber: Number(stepNumber), - timestamp: Number(timestamp), - contextWindowUsage: checkpoint.contextWindowUsage ?? 0, - } - }), - ) - return checkpoints.sort((a, b) => b.stepNumber - a.stepNumber) +): Promise<{ id: string; runId: string; stepNumber: number; contextWindowUsage: number }[]> { + const checkpoints = await getCheckpointsByJobId(jobId) + return checkpoints + .map((cp) => ({ + id: cp.id, + runId: cp.runId, + stepNumber: cp.stepNumber, + contextWindowUsage: cp.contextWindowUsage ?? 0, + })) + .sort((a, b) => b.stepNumber - a.stepNumber) } export async function getEventsWithDetails( jobId: string, diff --git a/packages/perstack/src/start.ts b/packages/perstack/src/start.ts index e1e4039d..883da62a 100644 --- a/packages/perstack/src/start.ts +++ b/packages/perstack/src/start.ts @@ -6,13 +6,13 @@ import { startCommandInputSchema, } from "@perstack/core" import { run, runtimeVersion } from "@perstack/runtime" -import type { CheckpointHistoryItem, EventHistoryItem, RunHistoryItem } from "@perstack/tui" +import type { CheckpointHistoryItem, EventHistoryItem, JobHistoryItem } from "@perstack/tui" import { renderStart } from "@perstack/tui" import { Command } from "commander" import { resolveRunContext } from "./lib/context.js" import { parseInteractiveToolCallResult } from "./lib/interactive.js" import { - getAllRuns, + getAllJobs, getCheckpointById, getCheckpointsWithDetails, getEventContents, @@ -72,15 +72,14 @@ export const startCommand = new Command() name: key, })) const recentExperts = await getRecentExperts(10) - const historyRuns: RunHistoryItem[] = showHistory - ? (await getAllRuns()).map((r) => ({ - jobId: r.jobId, - runId: r.runId, - expertKey: r.expertKey, - model: r.model, - inputText: r.input.text ?? "", - startedAt: r.startedAt, - updatedAt: r.updatedAt, + const historyJobs: JobHistoryItem[] = showHistory + ? (await getAllJobs()).map((j) => ({ + jobId: j.id, + status: j.status, + expertKey: j.coordinatorExpertKey, + totalSteps: j.totalSteps, + startedAt: j.startedAt, + finishedAt: j.finishedAt, })) : [] const resumeState: { checkpoint: CheckpointHistoryItem | null } = { checkpoint: null } @@ -106,7 +105,7 @@ export const startCommand = new Command() }, configuredExperts, recentExperts, - historyRuns, + historyJobs, onContinue: (query: string) => { if (resolveContinueQuery) { resolveContinueQuery(query) @@ -116,16 +115,16 @@ export const startCommand = new Command() onResumeFromCheckpoint: (cp: CheckpointHistoryItem) => { resumeState.checkpoint = cp }, - onLoadCheckpoints: async (r: RunHistoryItem): Promise => { - const checkpoints = await getCheckpointsWithDetails(r.jobId, r.runId) - return checkpoints.map((cp) => ({ ...cp, jobId: r.jobId })) + onLoadCheckpoints: async (j: JobHistoryItem): Promise => { + const checkpoints = await getCheckpointsWithDetails(j.jobId) + return checkpoints.map((cp) => ({ ...cp, jobId: j.jobId })) }, onLoadEvents: async ( - r: RunHistoryItem, + j: JobHistoryItem, cp: CheckpointHistoryItem, ): Promise => { - const events = await getEventsWithDetails(r.jobId, r.runId, cp.stepNumber) - return events.map((e) => ({ ...e, jobId: r.jobId })) + const events = await getEventsWithDetails(j.jobId, cp.runId, cp.stepNumber) + return events.map((e) => ({ ...e, jobId: j.jobId })) }, onLoadHistoricalEvents: async (cp: CheckpointHistoryItem) => { return await getEventContents(cp.jobId, cp.runId, cp.stepNumber) @@ -139,11 +138,7 @@ export const startCommand = new Command() } let currentCheckpoint = resumeState.checkpoint !== null - ? await getCheckpointById( - resumeState.checkpoint.jobId, - resumeState.checkpoint.runId, - resumeState.checkpoint.id, - ) + ? await getCheckpointById(resumeState.checkpoint.jobId, resumeState.checkpoint.id) : checkpoint if (currentCheckpoint && currentCheckpoint.expert.key !== finalExpertKey) { console.error( diff --git a/packages/runtime/src/default-store.test.ts b/packages/runtime/src/default-store.test.ts index ce4a98f9..36e8891c 100644 --- a/packages/runtime/src/default-store.test.ts +++ b/packages/runtime/src/default-store.test.ts @@ -46,35 +46,33 @@ function createTestEvent(overrides: Partial = {}): RunEvent { describe("@perstack/runtime: default-store", () => { const testJobId = `test-job-${Date.now()}` const testRunId = `test-run-${Date.now()}` - const testRunDir = `${process.cwd()}/perstack/jobs/${testJobId}/runs/${testRunId}` + const testJobDir = `${process.cwd()}/perstack/jobs/${testJobId}` + const testCheckpointDir = `${testJobDir}/checkpoints` + const testRunDir = `${testJobDir}/runs/${testRunId}` beforeEach(async () => { - await fs.rm(testRunDir, { recursive: true, force: true }) + await fs.rm(testJobDir, { recursive: true, force: true }) }) afterEach(async () => { - await fs.rm(testRunDir, { recursive: true, force: true }) + await fs.rm(testJobDir, { recursive: true, force: true }) }) describe("defaultStoreCheckpoint", () => { it("stores checkpoint to filesystem", async () => { const checkpoint = createTestCheckpoint({ jobId: testJobId, runId: testRunId }) - const timestamp = Date.now() - await defaultStoreCheckpoint(checkpoint, timestamp) - const expectedPath = path.join( - testRunDir, - `checkpoint-${timestamp}-${checkpoint.stepNumber}-${checkpoint.id}.json`, - ) + await defaultStoreCheckpoint(checkpoint) + const expectedPath = path.join(testCheckpointDir, `${checkpoint.id}.json`) const stored = JSON.parse(await fs.readFile(expectedPath, "utf-8")) expect(stored.id).toBe(checkpoint.id) expect(stored.runId).toBe(checkpoint.runId) }) - it("creates run directory if not exists", async () => { + it("creates checkpoints directory if not exists", async () => { const checkpoint = createTestCheckpoint({ jobId: testJobId, runId: testRunId }) - await defaultStoreCheckpoint(checkpoint, Date.now()) + await defaultStoreCheckpoint(checkpoint) const dirExists = await fs - .stat(testRunDir) + .stat(testCheckpointDir) .then(() => true) .catch(() => false) expect(dirExists).toBe(true) @@ -84,18 +82,16 @@ describe("@perstack/runtime: default-store", () => { describe("defaultRetrieveCheckpoint", () => { it("retrieves stored checkpoint by id", async () => { const checkpoint = createTestCheckpoint({ jobId: testJobId, runId: testRunId }) - const timestamp = Date.now() - await defaultStoreCheckpoint(checkpoint, timestamp) - const retrieved = await defaultRetrieveCheckpoint(testJobId, testRunId, checkpoint.id) + await defaultStoreCheckpoint(checkpoint) + const retrieved = await defaultRetrieveCheckpoint(testJobId, checkpoint.id) expect(retrieved.id).toBe(checkpoint.id) expect(retrieved.runId).toBe(checkpoint.runId) }) it("throws error when checkpoint not found", async () => { - await fs.mkdir(testRunDir, { recursive: true }) - await expect( - defaultRetrieveCheckpoint(testJobId, testRunId, "nonexistent-id"), - ).rejects.toThrow("checkpoint not found") + await expect(defaultRetrieveCheckpoint(testJobId, "nonexistent-id")).rejects.toThrow( + "checkpoint not found", + ) }) }) diff --git a/packages/runtime/src/default-store.ts b/packages/runtime/src/default-store.ts index e99a0219..ecdd6aa6 100644 --- a/packages/runtime/src/default-store.ts +++ b/packages/runtime/src/default-store.ts @@ -1,35 +1,35 @@ -import { mkdir, readdir, readFile, writeFile } from "node:fs/promises" +import { existsSync } from "node:fs" +import { mkdir, readFile, writeFile } from "node:fs/promises" import type { RunEvent } from "@perstack/core" import { type Checkpoint, checkpointSchema } from "@perstack/core" +import { getJobDir } from "./job-store.js" import { defaultGetRunDir as getRunDir } from "./run-setting-store.js" +export function getCheckpointDir(jobId: string): string { + return `${getJobDir(jobId)}/checkpoints` +} + +export function getCheckpointPath(jobId: string, checkpointId: string): string { + return `${getCheckpointDir(jobId)}/${checkpointId}.json` +} + export async function defaultRetrieveCheckpoint( jobId: string, - runId: string, checkpointId: string, ): Promise { - const runDir = getRunDir(jobId, runId) - const checkpointFiles = await readdir(runDir, { withFileTypes: true }).then((files) => - files.filter((file) => file.isFile() && file.name.startsWith("checkpoint-")), - ) - const checkpointFile = checkpointFiles.find((file) => file.name.endsWith(`-${checkpointId}.json`)) - if (!checkpointFile) { - throw new Error(`checkpoint not found: ${runId} ${checkpointId}`) + const checkpointPath = getCheckpointPath(jobId, checkpointId) + if (!existsSync(checkpointPath)) { + throw new Error(`checkpoint not found: ${checkpointId}`) } - const checkpointPath = `${runDir}/${checkpointFile.name}` const checkpoint = await readFile(checkpointPath, "utf8") return checkpointSchema.parse(JSON.parse(checkpoint)) } -export async function defaultStoreCheckpoint( - checkpoint: Checkpoint, - timestamp: number, -): Promise { - const { id, jobId, runId, stepNumber } = checkpoint - const runDir = getRunDir(jobId, runId) - const checkpointPath = `${runDir}/checkpoint-${timestamp}-${stepNumber}-${id}.json` - await mkdir(runDir, { recursive: true }) - await writeFile(checkpointPath, JSON.stringify(checkpoint)) +export async function defaultStoreCheckpoint(checkpoint: Checkpoint): Promise { + const { id, jobId } = checkpoint + const checkpointDir = getCheckpointDir(jobId) + await mkdir(checkpointDir, { recursive: true }) + await writeFile(getCheckpointPath(jobId, id), JSON.stringify(checkpoint)) } export async function defaultStoreEvent(event: RunEvent): Promise { diff --git a/packages/runtime/src/execute-state-machine.ts b/packages/runtime/src/execute-state-machine.ts index 559012ad..aae5a0f6 100644 --- a/packages/runtime/src/execute-state-machine.ts +++ b/packages/runtime/src/execute-state-machine.ts @@ -10,7 +10,7 @@ export type ExecuteStateMachineParams = { eventListener: (event: RunEvent | RuntimeEvent) => Promise skillManagers: Record eventEmitter: RunEventEmitter - storeCheckpoint: (checkpoint: Checkpoint, timestamp: number) => Promise + storeCheckpoint: (checkpoint: Checkpoint) => Promise shouldContinueRun?: (setting: RunSetting, checkpoint: Checkpoint, step: Step) => Promise } @@ -45,7 +45,7 @@ export async function executeStateMachine(params: ExecuteStateMachineParams): Pr } else { const event = await StateMachineLogics[runState.value](runState.context) if ("checkpoint" in event) { - await storeCheckpoint(event.checkpoint, event.timestamp) + await storeCheckpoint(event.checkpoint) } await eventEmitter.emit(event) if (shouldContinueRun) { diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 55de406f..4d2dcc16 100755 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,5 +1,7 @@ import pkg from "../package.json" with { type: "json" } +export { getCheckpointDir, getCheckpointPath } from "./default-store.js" +export * from "./job-store.js" export * from "./model.js" export * from "./runtime.js" export * from "./runtime-state-machine.js" diff --git a/packages/runtime/src/job-store.ts b/packages/runtime/src/job-store.ts new file mode 100644 index 00000000..9cd9691d --- /dev/null +++ b/packages/runtime/src/job-store.ts @@ -0,0 +1,44 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" +import path from "node:path" +import { type Job, jobSchema } from "@perstack/core" + +export function getJobDir(jobId: string): string { + return `${process.cwd()}/perstack/jobs/${jobId}` +} + +export function storeJob(job: Job): void { + const jobDir = getJobDir(job.id) + if (!existsSync(jobDir)) { + mkdirSync(jobDir, { recursive: true }) + } + const jobPath = path.resolve(jobDir, "job.json") + writeFileSync(jobPath, JSON.stringify(job, null, 2)) +} + +export function retrieveJob(jobId: string): Job | undefined { + const jobDir = getJobDir(jobId) + const jobPath = path.resolve(jobDir, "job.json") + if (!existsSync(jobPath)) { + return undefined + } + const content = readFileSync(jobPath, "utf-8") + return jobSchema.parse(JSON.parse(content)) +} + +export function createInitialJob(jobId: string, expertKey: string, maxSteps?: number): Job { + return { + id: jobId, + status: "running", + coordinatorExpertKey: expertKey, + totalSteps: 0, + maxSteps, + usage: { + inputTokens: 0, + outputTokens: 0, + reasoningTokens: 0, + totalTokens: 0, + cachedInputTokens: 0, + }, + startedAt: Date.now(), + } +} diff --git a/packages/runtime/src/runtime.ts b/packages/runtime/src/runtime.ts index 5fb94b1c..cd59db66 100755 --- a/packages/runtime/src/runtime.ts +++ b/packages/runtime/src/runtime.ts @@ -2,6 +2,7 @@ import { createId } from "@paralleldrive/cuid2" import { type Checkpoint, createRuntimeEvent, + type Job, type RunEvent, type RunParamsInput, type RunSetting, @@ -23,6 +24,7 @@ import { } from "./default-store.js" import { RunEventEmitter } from "./events/event-emitter.js" import { executeStateMachine } from "./execute-state-machine.js" +import { createInitialJob, retrieveJob, storeJob } from "./job-store.js" import { getContextWindow } from "./model.js" import { defaultGetRunDir, @@ -32,6 +34,7 @@ import { } from "./run-setting-store.js" import { type ResolveExpertToRunFn, setupExperts } from "./setup-experts.js" import { getSkillManagers } from "./skill-manager/index.js" +import { sumUsage } from "./usage.js" export async function run( runInput: RunParamsInput, @@ -41,8 +44,8 @@ export async function run( checkpoint: Checkpoint, step: Step, ) => Promise - retrieveCheckpoint?: (jobId: string, runId: string, checkpointId: string) => Promise - storeCheckpoint?: (checkpoint: Checkpoint, timestamp: number) => Promise + retrieveCheckpoint?: (jobId: string, checkpointId: string) => Promise + storeCheckpoint?: (checkpoint: Checkpoint) => Promise eventListener?: (event: RunEvent | RuntimeEvent) => void resolveExpertToRun?: ResolveExpertToRunFn fileSystem?: FileSystem @@ -59,6 +62,13 @@ export async function run( const contextWindow = getContextWindow(setting.providerConfig.providerName, setting.model) const getRunDir = options?.getRunDir ?? defaultGetRunDir await storeRunSetting(setting, options?.fileSystem, getRunDir) + let job: Job = + retrieveJob(setting.jobId) ?? + createInitialJob(setting.jobId, setting.expertKey, setting.maxSteps) + if (job.status !== "running") { + job = { ...job, status: "running", finishedAt: undefined } + } + storeJob(job) while (true) { const { expertToRun, experts } = await setupExperts(setting, options?.resolveExpertToRun) if (options?.eventListener) { @@ -100,12 +110,17 @@ export async function run( storeCheckpoint, shouldContinueRun: options?.shouldContinueRun, }) + job = { + ...job, + totalSteps: job.totalSteps + runResultCheckpoint.stepNumber, + usage: sumUsage(job.usage, runResultCheckpoint.usage), + } switch (runResultCheckpoint.status) { case "completed": { if (runResultCheckpoint.delegatedBy) { + storeJob(job) const parentCheckpoint = await retrieveCheckpoint( setting.jobId, - setting.runId, runResultCheckpoint.delegatedBy.checkpointId, ) const result = buildDelegationReturnState(setting, runResultCheckpoint, parentCheckpoint) @@ -113,21 +128,26 @@ export async function run( checkpoint = result.checkpoint break } + storeJob({ ...job, status: "completed", finishedAt: Date.now() }) return runResultCheckpoint } case "stoppedByInteractiveTool": { + storeJob({ ...job, status: "stoppedByInteractiveTool" }) return runResultCheckpoint } case "stoppedByDelegate": { + storeJob(job) const result = buildDelegateToState(setting, runResultCheckpoint, expertToRun) setting = result.setting checkpoint = result.checkpoint break } case "stoppedByExceededMaxSteps": { + storeJob({ ...job, status: "stoppedByMaxSteps", finishedAt: Date.now() }) return runResultCheckpoint } case "stoppedByError": { + storeJob({ ...job, status: "stoppedByError", finishedAt: Date.now() }) return runResultCheckpoint } default: diff --git a/packages/tui/apps/start/app.tsx b/packages/tui/apps/start/app.tsx index 0e4a16d2..777f9824 100644 --- a/packages/tui/apps/start/app.tsx +++ b/packages/tui/apps/start/app.tsx @@ -7,8 +7,8 @@ import type { EventHistoryItem, ExpertOption, InitialRuntimeConfig, + JobHistoryItem, PerstackEvent, - RunHistoryItem, } from "../../src/types/index.js" type AppProps = { @@ -19,13 +19,13 @@ type AppProps = { initialConfig: InitialRuntimeConfig configuredExperts?: ExpertOption[] recentExperts?: ExpertOption[] - historyRuns?: RunHistoryItem[] + historyJobs?: JobHistoryItem[] onComplete: (expertKey: string, query: string) => void onContinue?: (query: string) => void onResumeFromCheckpoint?: (checkpoint: CheckpointHistoryItem) => void - onLoadCheckpoints?: (run: RunHistoryItem) => Promise + onLoadCheckpoints?: (job: JobHistoryItem) => Promise onLoadEvents?: ( - run: RunHistoryItem, + job: JobHistoryItem, checkpoint: CheckpointHistoryItem, ) => Promise onLoadHistoricalEvents?: (checkpoint: CheckpointHistoryItem) => Promise diff --git a/packages/tui/apps/start/render.tsx b/packages/tui/apps/start/render.tsx index 03f88b04..30b25bd0 100644 --- a/packages/tui/apps/start/render.tsx +++ b/packages/tui/apps/start/render.tsx @@ -1,13 +1,13 @@ import { render } from "ink" -import { App } from "./app.js" import type { CheckpointHistoryItem, EventHistoryItem, InitialRuntimeConfig, + JobHistoryItem, PerstackEvent, - RunHistoryItem, } from "../../src/types/index.js" import { EventQueue } from "../../src/utils/event-queue.js" +import { App } from "./app.js" const createEventEmitter = () => { const eventQueue = new EventQueue() @@ -24,12 +24,12 @@ type RenderTuiInteractiveOptions = { initialConfig: InitialRuntimeConfig configuredExperts?: Array<{ key: string; name: string }> recentExperts?: Array<{ key: string; name: string; lastUsed?: number }> - historyRuns?: RunHistoryItem[] + historyJobs?: JobHistoryItem[] onContinue?: (query: string) => void onResumeFromCheckpoint?: (checkpoint: CheckpointHistoryItem) => void - onLoadCheckpoints?: (run: RunHistoryItem) => Promise + onLoadCheckpoints?: (job: JobHistoryItem) => Promise onLoadEvents?: ( - run: RunHistoryItem, + job: JobHistoryItem, checkpoint: CheckpointHistoryItem, ) => Promise onLoadHistoricalEvents?: (checkpoint: CheckpointHistoryItem) => Promise @@ -51,7 +51,7 @@ export async function renderStart( initialConfig={options.initialConfig} configuredExperts={options.configuredExperts} recentExperts={options.recentExperts} - historyRuns={options.historyRuns} + historyJobs={options.historyJobs} onComplete={(expertKey, query) => { resolved = true resolve({ diff --git a/packages/tui/src/components/browser-router.tsx b/packages/tui/src/components/browser-router.tsx index 255228bd..de4c941e 100644 --- a/packages/tui/src/components/browser-router.tsx +++ b/packages/tui/src/components/browser-router.tsx @@ -28,9 +28,9 @@ export const BrowserRouter = ({ inputState }: BrowserRouterProps) => { case "browsingHistory": return ( ) @@ -45,7 +45,7 @@ export const BrowserRouter = ({ inputState }: BrowserRouterProps) => { case "browsingCheckpoints": return ( void onCheckpointResume: (checkpoint: CheckpointHistoryItem) => void onBack: () => void } export const BrowsingCheckpointsInput = ({ - run, + job, checkpoints, onCheckpointSelect, onCheckpointResume, onBack, }: BrowsingCheckpointsInputProps) => ( ( - {isSelected ? ">" : " "} Step {cp.stepNumber} ({formatTimestamp(cp.timestamp)}) + {isSelected ? ">" : " "} Step {cp.stepNumber} ({cp.id}) )} /> diff --git a/packages/tui/src/components/input-areas/browsing-history.tsx b/packages/tui/src/components/input-areas/browsing-history.tsx index f635e827..f2805a74 100644 --- a/packages/tui/src/components/input-areas/browsing-history.tsx +++ b/packages/tui/src/components/input-areas/browsing-history.tsx @@ -1,29 +1,29 @@ import { Text } from "ink" -import { KEY_HINTS, UI_CONSTANTS } from "../../constants.js" -import { formatTimestamp, truncateText } from "../../helpers.js" -import type { RunHistoryItem } from "../../types/index.js" +import { KEY_HINTS } from "../../constants.js" +import { formatTimestamp } from "../../helpers.js" +import type { JobHistoryItem } from "../../types/index.js" import { ListBrowser } from "../list-browser.js" export type BrowsingHistoryInputProps = { - runs: RunHistoryItem[] - onRunSelect: (run: RunHistoryItem) => void - onRunResume: (run: RunHistoryItem) => void + jobs: JobHistoryItem[] + onJobSelect: (job: JobHistoryItem) => void + onJobResume: (job: JobHistoryItem) => void onSwitchToExperts: () => void } export const BrowsingHistoryInput = ({ - runs, - onRunSelect, - onRunResume, + jobs, + onJobSelect, + onJobResume, onSwitchToExperts, }: BrowsingHistoryInputProps) => ( { if (char === "c" && selected) { - onRunSelect(selected) + onJobSelect(selected) return true } if (char === "n") { @@ -32,11 +32,10 @@ export const BrowsingHistoryInput = ({ } return false }} - renderItem={(run, isSelected) => ( - - {isSelected ? ">" : " "} {run.expertKey} -{" "} - {truncateText(run.inputText, UI_CONSTANTS.TRUNCATE_TEXT_SHORT)} ( - {formatTimestamp(run.startedAt)}) + renderItem={(job, isSelected) => ( + + {isSelected ? ">" : " "} {job.expertKey} - {job.totalSteps} steps ({job.jobId}) ( + {formatTimestamp(job.startedAt)}) )} /> diff --git a/packages/tui/src/context/input-area-context.tsx b/packages/tui/src/context/input-area-context.tsx index 824b3547..99bdf024 100644 --- a/packages/tui/src/context/input-area-context.tsx +++ b/packages/tui/src/context/input-area-context.tsx @@ -3,13 +3,13 @@ import type { BrowsingEventsState, CheckpointHistoryItem, EventHistoryItem, - RunHistoryItem, + JobHistoryItem, } from "../types/index.js" export type InputAreaContextValue = { onExpertSelect: (expertKey: string) => void onQuerySubmit: (query: string) => void - onRunSelect: (run: RunHistoryItem) => void - onRunResume: (run: RunHistoryItem) => void + onJobSelect: (job: JobHistoryItem) => void + onJobResume: (job: JobHistoryItem) => void onCheckpointSelect: (checkpoint: CheckpointHistoryItem) => void onCheckpointResume: (checkpoint: CheckpointHistoryItem) => void onEventSelect: (state: BrowsingEventsState, event: EventHistoryItem) => void diff --git a/packages/tui/src/hooks/actions/use-history-actions.ts b/packages/tui/src/hooks/actions/use-history-actions.ts index 51fdeb7e..f36bbfcd 100644 --- a/packages/tui/src/hooks/actions/use-history-actions.ts +++ b/packages/tui/src/hooks/actions/use-history-actions.ts @@ -6,18 +6,18 @@ import type { EventHistoryItem, ExpertOption, InputState, + JobHistoryItem, PerstackEvent, - RunHistoryItem, } from "../../types/index.js" import { useErrorHandler } from "../core/use-error-handler.js" import type { InputAction } from "../state/use-input-state.js" type UseHistoryActionsOptions = { allExperts: ExpertOption[] - historyRuns?: RunHistoryItem[] - onLoadCheckpoints?: (run: RunHistoryItem) => Promise + historyJobs?: JobHistoryItem[] + onLoadCheckpoints?: (job: JobHistoryItem) => Promise onLoadEvents?: ( - run: RunHistoryItem, + job: JobHistoryItem, checkpoint: CheckpointHistoryItem, ) => Promise onResumeFromCheckpoint?: (checkpoint: CheckpointHistoryItem) => void @@ -32,7 +32,7 @@ type UseHistoryActionsOptions = { export const useHistoryActions = (options: UseHistoryActionsOptions) => { const { allExperts, - historyRuns, + historyJobs, onLoadCheckpoints, onLoadEvents, onResumeFromCheckpoint, @@ -44,16 +44,16 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => { setExpertName, onError, } = options - const [selectedRun, setSelectedRun] = useState(null) + const [selectedJob, setSelectedJob] = useState(null) const handleError = useErrorHandler(onError) - const handleRunSelect = useCallback( - async (run: RunHistoryItem) => { + const handleJobSelect = useCallback( + async (job: JobHistoryItem) => { try { - setSelectedRun(run) - setExpertName(run.expertKey) + setSelectedJob(job) + setExpertName(job.expertKey) if (onLoadCheckpoints) { - const checkpoints = await onLoadCheckpoints(run) - dispatch({ type: "SELECT_RUN", run, checkpoints }) + const checkpoints = await onLoadCheckpoints(job) + dispatch({ type: "SELECT_JOB", job, checkpoints }) } } catch (error) { handleError(error, "Failed to load checkpoints") @@ -61,13 +61,13 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => { }, [onLoadCheckpoints, dispatch, setExpertName, handleError], ) - const handleRunResume = useCallback( - async (run: RunHistoryItem) => { + const handleJobResume = useCallback( + async (job: JobHistoryItem) => { try { - setSelectedRun(run) - setExpertName(run.expertKey) + setSelectedJob(job) + setExpertName(job.expertKey) if (onLoadCheckpoints && onResumeFromCheckpoint) { - const checkpoints = await onLoadCheckpoints(run) + const checkpoints = await onLoadCheckpoints(job) const latestCheckpoint = checkpoints[0] if (latestCheckpoint) { if (onLoadHistoricalEvents) { @@ -76,12 +76,12 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => { } setCurrentStep(latestCheckpoint.stepNumber) setContextWindowUsage(latestCheckpoint.contextWindowUsage) - dispatch({ type: "RESUME_CHECKPOINT", expertKey: run.expertKey }) + dispatch({ type: "RESUME_CHECKPOINT", expertKey: job.expertKey }) onResumeFromCheckpoint(latestCheckpoint) } } } catch (error) { - handleError(error, "Failed to resume run") + handleError(error, "Failed to resume job") } }, [ @@ -99,15 +99,15 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => { const handleCheckpointSelect = useCallback( async (checkpoint: CheckpointHistoryItem) => { try { - if (selectedRun && onLoadEvents) { - const eventsData = await onLoadEvents(selectedRun, checkpoint) - dispatch({ type: "SELECT_CHECKPOINT", run: selectedRun, checkpoint, events: eventsData }) + if (selectedJob && onLoadEvents) { + const eventsData = await onLoadEvents(selectedJob, checkpoint) + dispatch({ type: "SELECT_CHECKPOINT", job: selectedJob, checkpoint, events: eventsData }) } } catch (error) { handleError(error, "Failed to load events") } }, - [selectedRun, onLoadEvents, dispatch, handleError], + [selectedJob, onLoadEvents, dispatch, handleError], ) const handleCheckpointResume = useCallback( async (checkpoint: CheckpointHistoryItem) => { @@ -118,7 +118,7 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => { } setCurrentStep(checkpoint.stepNumber) setContextWindowUsage(checkpoint.contextWindowUsage) - dispatch({ type: "RESUME_CHECKPOINT", expertKey: selectedRun?.expertKey || "" }) + dispatch({ type: "RESUME_CHECKPOINT", expertKey: selectedJob?.expertKey || "" }) onResumeFromCheckpoint(checkpoint) } }, @@ -129,25 +129,25 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => { setCurrentStep, setContextWindowUsage, dispatch, - selectedRun?.expertKey, + selectedJob?.expertKey, ], ) const handleBackFromEvents = useCallback(async () => { try { - if (selectedRun && onLoadCheckpoints) { - const checkpoints = await onLoadCheckpoints(selectedRun) - dispatch({ type: "GO_BACK_FROM_EVENTS", run: selectedRun, checkpoints }) + if (selectedJob && onLoadCheckpoints) { + const checkpoints = await onLoadCheckpoints(selectedJob) + dispatch({ type: "GO_BACK_FROM_EVENTS", job: selectedJob, checkpoints }) } } catch (error) { handleError(error, "Failed to go back from events") } - }, [selectedRun, onLoadCheckpoints, dispatch, handleError]) + }, [selectedJob, onLoadCheckpoints, dispatch, handleError]) const handleBackFromCheckpoints = useCallback(() => { - if (historyRuns) { - setSelectedRun(null) - dispatch({ type: "GO_BACK_FROM_CHECKPOINTS", historyRuns }) + if (historyJobs) { + setSelectedJob(null) + dispatch({ type: "GO_BACK_FROM_CHECKPOINTS", historyJobs }) } - }, [historyRuns, dispatch]) + }, [historyJobs, dispatch]) const handleEventSelect = useCallback( (state: BrowsingEventsState, event: EventHistoryItem) => { dispatch({ @@ -189,14 +189,14 @@ export const useHistoryActions = (options: UseHistoryActionsOptions) => { dispatch({ type: "BROWSE_EXPERTS", experts: allExperts }) }, [dispatch, allExperts]) const handleSwitchToHistory = useCallback(() => { - if (historyRuns) { - dispatch({ type: "BROWSE_HISTORY", runs: historyRuns }) + if (historyJobs) { + dispatch({ type: "BROWSE_HISTORY", jobs: historyJobs }) } - }, [dispatch, historyRuns]) + }, [dispatch, historyJobs]) return { - selectedRun, - handleRunSelect, - handleRunResume, + selectedJob, + handleJobSelect, + handleJobResume, handleCheckpointSelect, handleCheckpointResume, handleEventSelect, diff --git a/packages/tui/src/hooks/state/use-input-state.ts b/packages/tui/src/hooks/state/use-input-state.ts index 17ea5e6c..2e476fab 100644 --- a/packages/tui/src/hooks/state/use-input-state.ts +++ b/packages/tui/src/hooks/state/use-input-state.ts @@ -5,25 +5,25 @@ import type { EventHistoryItem, ExpertOption, InputState, - RunHistoryItem, + JobHistoryItem, } from "../../types/index.js" type InputAction = | { type: "SELECT_EXPERT"; expertKey: string; needsQuery: boolean } | { type: "START_RUN" } | { type: "END_RUN"; expertName: string; reason: "completed" | "stopped" } - | { type: "BROWSE_HISTORY"; runs: RunHistoryItem[] } + | { type: "BROWSE_HISTORY"; jobs: JobHistoryItem[] } | { type: "BROWSE_EXPERTS"; experts: ExpertOption[] } - | { type: "SELECT_RUN"; run: RunHistoryItem; checkpoints: CheckpointHistoryItem[] } + | { type: "SELECT_JOB"; job: JobHistoryItem; checkpoints: CheckpointHistoryItem[] } | { type: "SELECT_CHECKPOINT" - run: RunHistoryItem + job: JobHistoryItem checkpoint: CheckpointHistoryItem events: EventHistoryItem[] } | { type: "RESUME_CHECKPOINT"; expertKey: string } - | { type: "GO_BACK_FROM_EVENTS"; run: RunHistoryItem; checkpoints: CheckpointHistoryItem[] } - | { type: "GO_BACK_FROM_CHECKPOINTS"; historyRuns: RunHistoryItem[] } + | { type: "GO_BACK_FROM_EVENTS"; job: JobHistoryItem; checkpoints: CheckpointHistoryItem[] } + | { type: "GO_BACK_FROM_CHECKPOINTS"; historyJobs: JobHistoryItem[] } | { type: "INITIALIZE_RUNTIME" } | { type: "SELECT_EVENT" @@ -49,11 +49,11 @@ const inputReducer = (_state: InputState, action: InputAction): InputState => { case "END_RUN": return { type: "enteringQuery", expertName: action.expertName } case "BROWSE_HISTORY": - return { type: "browsingHistory", runs: action.runs } + return { type: "browsingHistory", jobs: action.jobs } case "BROWSE_EXPERTS": return { type: "browsingExperts", experts: action.experts } - case "SELECT_RUN": - return { type: "browsingCheckpoints", run: action.run, checkpoints: action.checkpoints } + case "SELECT_JOB": + return { type: "browsingCheckpoints", job: action.job, checkpoints: action.checkpoints } case "SELECT_CHECKPOINT": return { type: "browsingEvents", @@ -63,9 +63,9 @@ const inputReducer = (_state: InputState, action: InputAction): InputState => { case "RESUME_CHECKPOINT": return { type: "enteringQuery", expertName: action.expertKey } case "GO_BACK_FROM_EVENTS": - return { type: "browsingCheckpoints", run: action.run, checkpoints: action.checkpoints } + return { type: "browsingCheckpoints", job: action.job, checkpoints: action.checkpoints } case "GO_BACK_FROM_CHECKPOINTS": - return { type: "browsingHistory", runs: action.historyRuns } + return { type: "browsingHistory", jobs: action.historyJobs } case "SELECT_EVENT": return { type: "browsingEventDetail", @@ -89,11 +89,11 @@ type UseInputStateOptions = { initialExpertName?: string configuredExperts?: ExpertOption[] recentExperts?: ExpertOption[] - historyRuns?: RunHistoryItem[] + historyJobs?: JobHistoryItem[] } const getInitialState = (options: UseInputStateOptions): InputState => { - if (options.showHistory && options.historyRuns) { - return { type: "browsingHistory", runs: options.historyRuns } + if (options.showHistory && options.historyJobs) { + return { type: "browsingHistory", jobs: options.historyJobs } } if (options.needsQueryInput) { return { type: "enteringQuery", expertName: options.initialExpertName || "" } diff --git a/packages/tui/src/hooks/use-app-state.ts b/packages/tui/src/hooks/use-app-state.ts index 46916a8f..97f42617 100644 --- a/packages/tui/src/hooks/use-app-state.ts +++ b/packages/tui/src/hooks/use-app-state.ts @@ -5,8 +5,8 @@ import type { EventHistoryItem, ExpertOption, InitialRuntimeConfig, + JobHistoryItem, PerstackEvent, - RunHistoryItem, } from "../types/index.js" import { useExpertActions, useHistoryActions, useRunActions } from "./actions/index.js" import { useInputState } from "./state/use-input-state.js" @@ -21,13 +21,13 @@ type UseAppStateProps = { initialConfig: InitialRuntimeConfig configuredExperts?: ExpertOption[] recentExperts?: ExpertOption[] - historyRuns?: RunHistoryItem[] + historyJobs?: JobHistoryItem[] onComplete: (expertKey: string, query: string) => void onContinue?: (query: string) => void onResumeFromCheckpoint?: (checkpoint: CheckpointHistoryItem) => void - onLoadCheckpoints?: (run: RunHistoryItem) => Promise + onLoadCheckpoints?: (job: JobHistoryItem) => Promise onLoadEvents?: ( - run: RunHistoryItem, + job: JobHistoryItem, checkpoint: CheckpointHistoryItem, ) => Promise onLoadHistoricalEvents?: (checkpoint: CheckpointHistoryItem) => Promise @@ -42,7 +42,7 @@ export const useAppState = (props: UseAppStateProps) => { initialConfig, configuredExperts, recentExperts, - historyRuns, + historyJobs, onComplete, onContinue, onResumeFromCheckpoint, @@ -66,7 +66,7 @@ export const useAppState = (props: UseAppStateProps) => { initialExpertName, configuredExperts, recentExperts, - historyRuns, + historyJobs, }) const { markAsStarted, handleQuerySubmit } = useRunActions({ expertName: runtimeInfo.expertName, @@ -98,7 +98,7 @@ export const useAppState = (props: UseAppStateProps) => { }) const history = useHistoryActions({ allExperts, - historyRuns, + historyJobs, onLoadCheckpoints, onLoadEvents, onResumeFromCheckpoint, @@ -116,8 +116,8 @@ export const useAppState = (props: UseAppStateProps) => { () => ({ onExpertSelect: handleExpertSelect, onQuerySubmit: handleQuerySubmit, - onRunSelect: history.handleRunSelect, - onRunResume: history.handleRunResume, + onJobSelect: history.handleJobSelect, + onJobResume: history.handleJobResume, onCheckpointSelect: history.handleCheckpointSelect, onCheckpointResume: history.handleCheckpointResume, onEventSelect: history.handleEventSelect, @@ -128,8 +128,8 @@ export const useAppState = (props: UseAppStateProps) => { [ handleExpertSelect, handleQuerySubmit, - history.handleRunSelect, - history.handleRunResume, + history.handleJobSelect, + history.handleJobResume, history.handleCheckpointSelect, history.handleCheckpointResume, history.handleEventSelect, diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 9977132e..49dda98a 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -6,6 +6,7 @@ export { renderUnpublish, type UnpublishWizardResult } from "../apps/unpublish/r export type { CheckpointHistoryItem, EventHistoryItem, + JobHistoryItem, PerstackEvent, RunHistoryItem, WizardExpertChoice, diff --git a/packages/tui/src/types/base.ts b/packages/tui/src/types/base.ts index e7d33b5c..a134155f 100644 --- a/packages/tui/src/types/base.ts +++ b/packages/tui/src/types/base.ts @@ -54,12 +54,19 @@ export type RunHistoryItem = { startedAt: number updatedAt: number } +export type JobHistoryItem = { + jobId: string + status: string + expertKey: string + totalSteps: number + startedAt: number + finishedAt?: number +} export type CheckpointHistoryItem = { id: string jobId: string runId: string stepNumber: number - timestamp: number contextWindowUsage: number } export type EventHistoryItem = { diff --git a/packages/tui/src/types/index.ts b/packages/tui/src/types/index.ts index fd78dc50..6c6f1ea2 100644 --- a/packages/tui/src/types/index.ts +++ b/packages/tui/src/types/index.ts @@ -5,6 +5,7 @@ export type { EventResult, ExpertOption, InitialRuntimeConfig, + JobHistoryItem, PerstackEvent, RunHistoryItem, RuntimeInfo, diff --git a/packages/tui/src/types/input-state.ts b/packages/tui/src/types/input-state.ts index aa2bbe70..780b83ca 100644 --- a/packages/tui/src/types/input-state.ts +++ b/packages/tui/src/types/input-state.ts @@ -2,7 +2,7 @@ import type { CheckpointHistoryItem, EventHistoryItem, ExpertOption, - RunHistoryItem, + JobHistoryItem, } from "./base.js" export type EnteringQueryState = { @@ -14,7 +14,7 @@ export type RunningState = { } export type BrowsingHistoryState = { type: "browsingHistory" - runs: RunHistoryItem[] + jobs: JobHistoryItem[] } export type BrowsingExpertsState = { type: "browsingExperts" @@ -22,7 +22,7 @@ export type BrowsingExpertsState = { } export type BrowsingCheckpointsState = { type: "browsingCheckpoints" - run: RunHistoryItem + job: JobHistoryItem checkpoints: CheckpointHistoryItem[] } export type BrowsingEventsState = {