1import type {
2 CheckpointId,
3 CheckpointRef,
4 CheckpointScopeId,
5 MessageId,
6 OrchestrationV2CheckpointFileTotals,
7 OrchestrationV2ThreadProjection,
8 RunId,
9} from "@t3tools/contracts";
10import * as DateTime from "effect/DateTime";
11
12export interface ThreadCheckpointSummary {
13 readonly checkpointId?: CheckpointId;
14 readonly scopeId?: CheckpointScopeId;
15 readonly runId: RunId;
16 readonly checkpointTurnCount: number;
17 readonly checkpointRef: CheckpointRef;
18 readonly status: "ready" | "missing" | "error" | "stale";
19 /** A prefix of the changed files when `filesTotal` is set; see `checkpointFileTotals`. */
20 readonly files: ReadonlyArray<{
21 readonly path: string;
22 readonly kind: string;
23 readonly additions: number;
24 readonly deletions: number;
25 }>;
26 readonly filesTotal?: OrchestrationV2CheckpointFileTotals;
27 readonly assistantMessageId: MessageId | null;
28 readonly completedAt: string;
29}
30
31export function deriveThreadCheckpointSummaries(
32 projection: Pick<OrchestrationV2ThreadProjection, "checkpoints" | "messages">,
33): ReadonlyArray<ThreadCheckpointSummary> {
34 let assistantMessageIdsByRun: Map<RunId, MessageId | null | undefined> | undefined;
35 let nextMessageIndex = -1;
36 return projection.checkpoints.flatMap((checkpoint) => {
37 if (checkpoint.appRunOrdinal === null || checkpoint.runId === null) return [];
38 const messages = projection.messages;
39 if (assistantMessageIdsByRun === undefined) {
40 assistantMessageIdsByRun = new Map();
41 for (const entry of projection.checkpoints) {
42 if (entry.appRunOrdinal !== null && entry.runId !== null) {
43 assistantMessageIdsByRun.set(entry.runId, undefined);
44 }
45 }
46 nextMessageIndex = messages.length - 1;
47 }
48 // Resume the reverse scan only as far as this checkpoint needs. Recent
49 // checkpoints must not force a traversal of unrelated older history.
50 while (nextMessageIndex >= 0 && assistantMessageIdsByRun.get(checkpoint.runId) === undefined) {
51 const message = messages[nextMessageIndex--]!;
52 const { runId, role } = message;
53 if (
54 role === "assistant" &&
55 runId !== null &&
56 assistantMessageIdsByRun.has(runId) &&
57 assistantMessageIdsByRun.get(runId) === undefined
58 ) {
59 assistantMessageIdsByRun.set(runId, message.id ?? null);
60 }
61 }
62 const assistantMessageId = assistantMessageIdsByRun.get(checkpoint.runId) ?? null;
63 return [
64 {
65 checkpointId: checkpoint.id,
66 scopeId: checkpoint.scopeId,
67 runId: checkpoint.runId,
68 checkpointTurnCount: checkpoint.appRunOrdinal,
69 checkpointRef: checkpoint.ref,
70 status: checkpoint.status,
71 files: checkpoint.files,
72 ...(checkpoint.filesTotal === undefined ? {} : { filesTotal: checkpoint.filesTotal }),
73 assistantMessageId,
74 completedAt: DateTime.formatIso(checkpoint.capturedAt),
75 },
76 ];
77 });
78}
79
80export function checkpointFileTotals(
81 checkpoint: Pick<ThreadCheckpointSummary, "files" | "filesTotal">,
82): OrchestrationV2CheckpointFileTotals {
83 if (checkpoint.filesTotal !== undefined) return checkpoint.filesTotal;
84 let additions = 0;
85 let deletions = 0;
86 for (const file of checkpoint.files) {
87 additions += file.additions;
88 deletions += file.deletions;
89 }
90 return { count: checkpoint.files.length, additions, deletions };
91}