1import { isOrchestrationV2WorkActive, type OrchestrationV2Subagent } from "@t3tools/contracts";
2
3/** Unknown settled timing must not turn a task's age into its work duration. */
4export function deriveSubagentElapsedMs(
5 agent: {
6 readonly status: OrchestrationV2Subagent["status"];
7 readonly startedAt: string | null;
8 readonly completedAt: string | null;
9 },
10 nowMs: number,
11): number | null {
12 if (agent.startedAt === null) return null;
13 const end = isOrchestrationV2WorkActive(agent.status)
14 ? nowMs
15 : agent.completedAt === null
16 ? null
17 : Date.parse(agent.completedAt);
18 if (end === null) return null;
19 const start = Date.parse(agent.startedAt);
20 return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null;
21}
22
23type LatestRunTiming = {
24 readonly runId: string | null;
25 /** Set when the turn is created; `startedAt` waits for the provider. */
26 readonly requestedAt?: string | null;
27 readonly startedAt: string | null;
28 readonly completedAt: string | null;
29};
30
31type RuntimeActivityState = {
32 readonly orchestrationStatus: string;
33 readonly activeRunId?: string | null;
34};
35
36export function formatDuration(durationMs: number): string {
37 if (!Number.isFinite(durationMs) || durationMs < 0) return "0ms";
38 if (durationMs < 1_000) return `${Math.max(1, Math.round(durationMs))}ms`;
39 if (durationMs < 10_000) {
40 const tenths = Math.round(durationMs / 100) / 10;
41 return tenths >= 10 ? "10s" : `${tenths.toFixed(1)}s`;
42 }
43 if (durationMs < 60_000) return `${Math.round(durationMs / 1_000)}s`;
44 const totalSeconds = Math.round(durationMs / 1_000);
45 const hours = Math.floor(totalSeconds / 3_600);
46 const minutes = Math.floor((totalSeconds % 3_600) / 60);
47 const seconds = totalSeconds % 60;
48 const parts: string[] = [];
49 if (hours > 0) parts.push(`${hours}h`);
50 if (minutes > 0) parts.push(`${minutes}m`);
51 if (seconds > 0) parts.push(`${seconds}s`);
52 return parts.join(" ");
53}
54
55function isLatestRunSettled(
56 latestRun: LatestRunTiming | null,
57 runtime: RuntimeActivityState | null,
58): boolean {
59 if (!latestRun) return false;
60 if (!latestRun.completedAt) return false;
61 if (!runtime) return true;
62 if (runtime.orchestrationStatus === "running") return false;
63 return true;
64}
65
66/**
67 * When the working indicator should be counting, and from when.
68 *
69 * `requestedAt` is the floor for an unsettled turn. The projector only stamps
70 * `startedAt` in the same update that moves the session to "running", so while
71 * the provider spins up (session "starting") a requested turn has no
72 * `startedAt` at all — and returning null there blinks the indicator out for
73 * the whole spin-up. A settled turn still falls through to `sendStartedAt`, so
74 * this cannot leave the indicator counting after the work is done.
75 */
76export function deriveActiveWorkStartedAt(
77 latestRun: LatestRunTiming | null,
78 runtime: RuntimeActivityState | null,
79 sendStartedAt: string | null,
80): string | null {
81 if (runtime?.activeRunId && runtime.activeRunId !== latestRun?.runId) {
82 return sendStartedAt;
83 }
84 if (!isLatestRunSettled(latestRun, runtime)) {
85 return latestRun?.startedAt ?? latestRun?.requestedAt ?? sendStartedAt;
86 }
87 return sendStartedAt;
88}