Skip to content
Source
Sign inSign up

threadExecution.ts

711 lines26.1 KB

1import {
2 latestExecutedRun,
3 latestRootProviderFailure,
4 latestUnheldRun,
5 threadErrorSummary,
6 usageLimitRunPresentedAsLatest,
7} from "@t3tools/shared/orchestrationV2ThreadError";
8import {
9 isOrchestrationV2WorkActive,
10 isProviderNativeSubagentThread,
11 latestProviderTurnForAttempt,
12 PROVIDER_DISPLAY_NAMES,
13 type ModelSelection,
14 type OrchestrationV2NotificationSource,
15 type OrchestrationV2PendingBackgroundTask,
16 type OrchestrationV2ProviderGoal,
17 type OrchestrationV2ProviderSession,
18 type ServerProvider,
19 type ServerProviderModel,
20 type OrchestrationV2ExecutionNode,
21 type OrchestrationV2LimitRecovery,
22 type OrchestrationV2ThreadProjection,
23 orchestrationV2RunWorkStartedAt,
24 type RunId,
25 type ThreadId,
26} from "@t3tools/contracts";
27import {
28 backgroundWorkHoldsCompletion,
29 derivePendingBackgroundWork,
30} from "@t3tools/shared/orchestrationV2PendingBackgroundWork";
31import {
32 getModelSelectionStringOptionValue,
33 getProviderOptionCurrentLabel,
34 getProviderOptionStatedValue,
35 resolveSelectableModel,
36 withLegacyUltracodeEffort,
37} from "@t3tools/shared/model";
38import { formatDuration } from "@t3tools/shared/orchestrationTiming";
39import * as DateTime from "effect/DateTime";
40​
41import {
42 threadRuntimeIsActive,
43 type ThreadRunSummary,
44 type ThreadRuntimeSummary,
45 type ThreadStructure,
46 type ThreadStructureRuntime,
47 type ThreadStructureTurnItem,
48} from "./models.ts";
49import { commandDisplayText } from "../work-log/commandLabel.ts";
50import { formatSubagentDisplayTitle } from "./subagentDisplay.ts";
51​
52const ACTIVITY_RUN_STATUSES = new Set(["preparing", "starting", "running", "waiting"]);
53const INTERRUPTIBLE_RUN_STATUSES = new Set(["preparing", "starting", "running"]);
54// Preparing runs show their workspace setup instead.
55const PROVIDER_STARTING_RUN_STATUSES = new Set(["starting", "running"]);
56​
57const EMPTY_EXTENSION_STATUSES: NonNullable<OrchestrationV2ProviderSession["extensionStatuses"]> =
58 [];
59​
60/** Only the selected provider's current session can supply transient extension status. */
61export function deriveProviderExtensionStatuses(
62 projection:
63 | (Pick<ThreadStructure, "thread"> & {
64 readonly providerThreads: ReadonlyArray<
65 Pick<
66 ThreadStructure["providerThreads"][number],
67 "id" | "providerSessionId" | "providerInstanceId"
68 >
69 >;
70 readonly providerSessions: ReadonlyArray<
71 Pick<
72 OrchestrationV2ProviderSession,
73 "id" | "providerInstanceId" | "status" | "extensionStatuses"
74 >
75 >;
76 })
77 | null,
78) {
79 if (projection === null) return EMPTY_EXTENSION_STATUSES;
80 const instanceId = projection.thread.modelSelection.instanceId;
81 const activeThread = projection.providerThreads.find(
82 (thread) => thread.id === projection.thread.activeProviderThreadId,
83 );
84 if (activeThread !== undefined && activeThread.providerInstanceId !== instanceId)
85 return EMPTY_EXTENSION_STATUSES;
86 const session = projection.providerSessions.findLast(
87 (session) =>
88 session.providerInstanceId === instanceId &&
89 (activeThread === undefined || session.id === activeThread.providerSessionId),
90 );
91 return session === undefined || session.status === "stopped"
92 ? EMPTY_EXTENSION_STATUSES
93 : (session.extensionStatuses ?? EMPTY_EXTENSION_STATUSES);
95​
96function latestMatchingRun(
97 projection: ThreadStructure,
98 predicate: (run: OrchestrationV2ThreadProjection["runs"][number]) => boolean,
99): OrchestrationV2ThreadProjection["runs"][number] | null {
100 return projection.runs.reduce<OrchestrationV2ThreadProjection["runs"][number] | null>(
101 (latest, candidate) =>
102 predicate(candidate) && (latest === null || candidate.ordinal > latest.ordinal)
103 ? candidate
104 : latest,
105 null,
106 );
108​
109function providerStartingLabel(
110 projection: ThreadStructure,
111 run: OrchestrationV2ThreadProjection["runs"][number],
112): string {
113 const driver = projection.providerSessions.findLast(
114 (session) => session.providerInstanceId === run.providerInstanceId,
115 )?.driver;
116 const name = driver === undefined ? undefined : PROVIDER_DISPLAY_NAMES[driver];
117 return name === undefined ? "Starting" : `Starting ${name}`;
119​
120function summarizeThreadRun(
121 projection: ThreadStructure,
122 run: OrchestrationV2ThreadProjection["runs"][number],
123): ThreadRunSummary {
124 const turnStatus = ACTIVITY_RUN_STATUSES.has(run.status)
125 ? latestProviderTurnForAttempt(projection.providerTurns, run.activeAttemptId)?.status
126 : undefined;
127 return {
128 runId: run.id,
129 status: run.status,
130 requestedAt: DateTime.formatIso(run.requestedAt),
131 startedAt: run.startedAt === null ? null : DateTime.formatIso(run.startedAt),
132 completedAt: run.completedAt === null ? null : DateTime.formatIso(run.completedAt),
133 assistantMessageId:
134 projection.messages.findLast(
135 (message) => message.runId === run.id && message.role === "assistant",
136 )?.id ?? null,
137 // A run counts as running before its provider opens; until a turn appears,
138 // the provider can still be launching for minutes.
139 ...(turnStatus === undefined
140 ? PROVIDER_STARTING_RUN_STATUSES.has(run.status)
141 ? { providerStartingLabel: providerStartingLabel(projection, run) }
142 : {}
143 : turnStatus === "pending" || turnStatus === "running"
144 ? {}
145 : { turnEnded: true }),
146 ...(run.sourcePlanRef === undefined ? {} : { sourcePlanRef: run.sourcePlanRef }),
147 };
149​
150function presentedUsageLimitRun(
151 projection: ThreadStructure,
152): OrchestrationV2ThreadProjection["runs"][number] | null {
153 const providerSession = projection.providerSessions.findLast(
154 (session) => session.providerInstanceId === projection.thread.providerInstanceId,
155 );
156 return usageLimitRunPresentedAsLatest(
157 projection.runs,
158 projection.turnItems,
159 providerSession?.lastError ?? null,
160 );
162​
163function presentedLatestRun(
164 projection: ThreadStructure,
165): OrchestrationV2ThreadProjection["runs"][number] | null {
166 return presentedUsageLimitRun(projection) ?? latestUnheldRun(projection.runs);
168​
169export function deriveLatestThreadRun(projection: ThreadStructure): ThreadRunSummary | null {
170 const run = presentedLatestRun(projection);
171 return run === null ? null : summarizeThreadRun(projection, run);
173​
174export function deriveThreadActivityRun(projection: ThreadStructure): ThreadRunSummary | null {
175 const run =
176 latestMatchingRun(projection, (candidate) => ACTIVITY_RUN_STATUSES.has(candidate.status)) ??
177 presentedLatestRun(projection);
178 return run === null ? null : summarizeThreadRun(projection, run);
180​
181/**
182 * Provider-native subagent threads never get app runs: their work is a runless
183 * root turn whose status follows the subagent.
184 */
185export function deriveRunlessWorkStartedAt(projection: ThreadStructure): string | null {
186 const status = deriveProviderSubagentStatus(projection);
187 return status !== null && isOrchestrationV2WorkActive(status.status) ? status.startedAt : null;
189​
190export interface ProviderSubagentStatus {
191 readonly status: OrchestrationV2ExecutionNode["status"];
192 readonly startedAt: string | null;
193 readonly completedAt: string | null;
195​
196export function deriveProviderSubagentStatus(
197 projection: ThreadStructure,
198): ProviderSubagentStatus | null {
199 if (!isProviderNativeSubagentThread(projection.thread)) return null;
200 const node = projection.nodes.findLast(
201 (candidate) => candidate.kind === "root_turn" && candidate.runId === null,
202 );
203 if (node === undefined) return null;
204 return {
205 status: node.status,
206 startedAt: node.startedAt === null ? null : DateTime.formatIso(node.startedAt),
207 completedAt: node.completedAt === null ? null : DateTime.formatIso(node.completedAt),
208 };
210​
211export function deriveReportedModelSelection(projection: ThreadStructure): ModelSelection | null {
212 const providerThread = projection.providerThreads.find(
213 (candidate) =>
214 candidate.id === projection.thread.activeProviderThreadId &&
215 candidate.providerInstanceId === projection.thread.modelSelection.instanceId,
216 );
217 return providerThread?.nativeMetadata?.modelSelection ?? null;
219​
220export const REASONING_EFFORT_OPTION_IDS: ReadonlyArray<string> = [
221 "reasoningEffort",
222 "effort",
223 "reasoning",
224 "variant",
225 "reasoning_effort",
226 "thinking_level",
227 "thinking",
228];
229​
230export function formatModelSelectionEffort(
231 selection: ModelSelection,
232 models: ReadonlyArray<ServerProviderModel> = [],
233 reportedSelection?: ModelSelection | null,
234): string | null {
235 const caps = models.find((model) => model.slug === selection.model)?.capabilities;
236 if (!caps) return null;
237 const options = withLegacyUltracodeEffort(caps.optionDescriptors, selection.options);
238 const current = options ? { ...selection, options } : selection;
239 const effortIds = [
240 ...REASONING_EFFORT_OPTION_IDS,
241 ...(caps.optionDescriptors
242 ?.filter((descriptor) => descriptor.type === "select" && descriptor.role === "reasoning")
243 .map((descriptor) => descriptor.id) ?? []),
244 ];
245 for (const id of effortIds) {
246 const descriptor = caps.optionDescriptors?.find((candidate) => candidate.id === id);
247 if (descriptor?.type !== "select") continue;
248 const value = getProviderOptionStatedValue(id, current, reportedSelection);
249 if (value === undefined && id !== "variant") continue;
250 if (typeof value === "boolean") continue;
251 // Bypass picker normalization so an obsolete choice cannot become a catalog default.
252 const label = getProviderOptionCurrentLabel(
253 { ...descriptor, currentValue: value },
254 current,
255 reportedSelection,
256 );
257 if (label) return label;
258 }
259 return null;
261​
262/** A reported model the picker catalog does not know, such as `claude-opus-5[1m]`, keeps its raw effort. */
263export function formatSubagentEffort(input: {
264 readonly modelSlug: string | null;
265 readonly modelSelection: ModelSelection | undefined;
266 readonly provider: Pick<ServerProvider, "driver" | "models"> | undefined;
267}): string | null {
268 const { modelSlug, modelSelection: selection, provider } = input;
269 if (!provider || !selection || !modelSlug) return null;
270 const selectionSlug =
271 resolveSelectableModel(provider.driver, selection.model, provider.models) ??
272 selection.model.trim();
273 if (selectionSlug !== modelSlug) return null;
274 if (provider.models.some((model) => model.slug === modelSlug)) {
275 return formatModelSelectionEffort({ ...selection, model: modelSlug }, provider.models);
276 }
277 return (
278 REASONING_EFFORT_OPTION_IDS.map((id) => getModelSelectionStringOptionValue(selection, id)).find(
279 Boolean,
280 ) ?? null
281 );
283​
284const SUBAGENT_STATUS_LABELS: Record<OrchestrationV2ExecutionNode["status"], string> = {
285 idle: "Idle",
286 pending: "Working",
287 running: "Working",
288 waiting: "Waiting",
289 completed: "Completed",
290 interrupted: "Interrupted",
291 failed: "Failed",
292 cancelled: "Cancelled",
293 rolled_back: "Cancelled",
294};
295​
296export function formatProviderSubagentStatus(
297 status: ProviderSubagentStatus | null,
298 nowMs: number,
299): string {
300 if (status === null) return "Starting";
301 const label = SUBAGENT_STATUS_LABELS[status.status];
302 const live = isOrchestrationV2WorkActive(status.status);
303 if (!live && status.status !== "completed") return label;
304 const start = status.startedAt === null ? Number.NaN : Date.parse(status.startedAt);
305 const end = live
306 ? nowMs
307 : status.completedAt === null
308 ? Number.NaN
309 : Date.parse(status.completedAt);
310 if (!Number.isFinite(start) || !Number.isFinite(end)) return label;
311 const elapsed = formatDuration(Math.max(1_000, Math.floor((end - start) / 1_000) * 1_000));
312 return live ? `${label} ${elapsed}` : `${label} in ${elapsed}`;
314​
315/** The run a manual continuation resumes; the server holds the queue until it does. */
316export function deriveResumableRunId(
317 projection: Pick<ThreadStructure, "runs" | "turnItems">,
318 runtime: Pick<ThreadStructureRuntime, "lastErrorClass"> | null,
319): RunId | null {
320 const run = latestExecutedRun(projection.runs);
321 if (run?.status === "interrupted") return run.id;
322 return run?.status === "failed" &&
323 runtime?.lastErrorClass === "usage_limit" &&
324 latestRootProviderFailure(run, projection.turnItems)?.class === "usage_limit"
325 ? run.id
326 : null;
328​
329export function deriveThreadRuntime(
330 projection: ThreadStructure & Pick<OrchestrationV2ThreadProjection, "updatedAt">,
331): ThreadRuntimeSummary | null {
332 const runtime = deriveThreadStructureRuntime(projection);
333 return runtime === null
334 ? null
335 : { ...runtime, updatedAt: DateTime.formatIso(projection.updatedAt) };
337​
338/** The runtime summary a thread's structure determines: all of it but `updatedAt`. */
339export function deriveThreadStructureRuntime(
340 projection: ThreadStructure,
341): ThreadStructureRuntime | null {
342 const latestRun = deriveLatestThreadRun(projection);
343 const providerSession = projection.providerSessions.findLast(
344 (session) => session.providerInstanceId === projection.thread.providerInstanceId,
345 );
346 const usageLimitedRun = presentedUsageLimitRun(projection);
347 const latestRunProjection = presentedLatestRun(projection);
348 const activityRun = deriveThreadActivityRun(projection);
349 const liveActivityRun = latestMatchingRun(projection, (run) =>
350 ACTIVITY_RUN_STATUSES.has(run.status),
351 );
352 const backgroundWorkHoldsRun = backgroundWorkHoldsCompletion(
353 derivePendingBackgroundWork({
354 latestRun: latestRunProjection,
355 providerThreads: projection.providerThreads,
356 turnItems: projection.turnItems,
357 activeProviderThreadId: projection.thread.activeProviderThreadId,
358 runs: projection.runs,
359 pullRequests: projection.thread.pullRequests,
360 }),
361 );
362 // A pull request watch can hold a thread that never ran.
363 if (
364 latestRun === null &&
365 projection.thread.activeProviderThreadId === null &&
366 !backgroundWorkHoldsRun
367 ) {
368 return null;
369 }
370 const activeRunId =
371 latestMatchingRun(projection, (run) => INTERRUPTIBLE_RUN_STATUSES.has(run.status))?.id ?? null;
372 return {
373 status: usageLimitedRun
374 ? "failed"
375 : backgroundWorkHoldsRun && latestRunProjection?.status !== "failed"
376 ? "idle"
377 : (activityRun?.status ?? "idle"),
378 activeRunId,
379 activityStartedAt:
380 liveActivityRun === null
381 ? null
382 : DateTime.formatIso(orchestrationV2RunWorkStartedAt(liveActivityRun)),
383 providerInstanceId: projection.thread.providerInstanceId,
384 providerName: providerSession?.driver ?? null,
385 ...threadErrorSummary(
386 latestRootProviderFailure(latestRunProjection, projection.turnItems),
387 providerSession?.lastError ?? null,
388 ),
389 };
391​
392export interface UsageLimitNoticeRows {
393 readonly itemIds: ReadonlySet<ThreadStructureTurnItem["id"]>;
394 /** Hidden only when its text reads as the provider's limit message; see PROVIDER_LIMIT_NOTICE. */
395 readonly providerNoticeItemId: ThreadStructureTurnItem["id"] | null;
397​
398// Claude Code's own limit messages, such as "You've hit your 5-hour limit · resets 7:10am".
399const PROVIDER_LIMIT_NOTICE =
400 /^(?:You[’']ve hit your (?:(?:5-hour|session|weekly|usage) )?limit|You[’']re out of extra usage|Claude AI usage limit reached)(?: · resets (?:[1-9]|1[0-2])(?::[0-5]\d)?(?:am|pm)(?: \([A-Za-z0-9_+-]+(?:\/[A-Za-z0-9_+-]+)*\))?)?\.?$/i;
401​
402/**
403 * The timeline rows that repeat a usage-limit pause while its recovery notice shows: the run's
404 * limit failure, Claude's pause notices, and the provider's own limit message. They return to
405 * history once another run executes.
406 */
407export function deriveUsageLimitNoticeRows(
408 projection: ThreadStructure,
409): UsageLimitNoticeRows | null {
410 const runtime = deriveThreadStructureRuntime(projection);
411 const run = presentedLatestRun(projection);
412 if (runtime?.status !== "failed" || runtime.lastErrorClass !== "usage_limit" || run === null) {
413 return null;
414 }
415 const failure = latestRootProviderFailure(run, projection.turnItems);
416 const itemIds = new Set<ThreadStructureTurnItem["id"]>();
417 let failedProviderTurnId: ThreadStructureTurnItem["providerTurnId"] = null;
418 for (const item of projection.turnItems) {
419 if (item.runId !== run.id) continue;
420 if (
421 item.type === "error" &&
422 item.status === "failed" &&
423 item.nodeId === run.rootNodeId &&
424 item.failure.class === "usage_limit"
425 ) {
426 itemIds.add(item.id);
427 if (item.failure === failure) failedProviderTurnId = item.providerTurnId;
428 }
429 }
430 let providerNotice: ThreadStructureTurnItem | null = null;
431 if (failedProviderTurnId !== null) {
432 for (const item of projection.turnItems) {
433 if (item.runId !== run.id || item.providerTurnId !== failedProviderTurnId) continue;
434 if (
435 item.type === "system_notice" &&
436 item.nodeId === run.rootNodeId &&
437 item.parentItemId === null &&
438 item.nativeItemRef?.driver === "claudeAgent" &&
439 item.nativeItemRef.nativeId?.startsWith(`usage-limit:${failedProviderTurnId}:`) === true
440 ) {
441 itemIds.add(item.id);
442 }
443 if (
444 item.type === "assistant_message" &&
445 item.parentItemId === null &&
446 (providerNotice === null || item.ordinal > providerNotice.ordinal)
447 ) {
448 providerNotice = item;
449 }
450 }
451 }
452 return { itemIds, providerNoticeItemId: providerNotice?.id ?? null };
454​
455export function withoutUsageLimitNoticeRows<
456 Row extends OrchestrationV2ThreadProjection["visibleTurnItems"][number],
457>(rows: ReadonlyArray<Row>, notice: UsageLimitNoticeRows | null): ReadonlyArray<Row> {
458 if (notice === null) return rows;
459 const hidden = ({ item }: Row) =>
460 notice.itemIds.has(item.id) ||
461 (item.id === notice.providerNoticeItemId &&
462 item.type === "assistant_message" &&
463 PROVIDER_LIMIT_NOTICE.test(item.text.trim()));
464 return rows.some(hidden) ? rows.filter((row) => !hidden(row)) : rows;
466​
467/** Whether a usage-limit recovery holds the thread's snooze, so its notice offers Wake now. */
468export function limitRecoveryOwnsSnooze(input: {
469 readonly runId: RunId | null | undefined;
470 readonly resetAt: string | null | undefined;
471 readonly recovery: OrchestrationV2LimitRecovery | null | undefined;
472 readonly snoozedUntil: string | null | undefined;
473}): boolean {
474 const { runId, resetAt, recovery, snoozedUntil } = input;
475 return (
476 recovery?.snooze === true &&
477 runId != null &&
478 recovery.runId === runId &&
479 resetAt != null &&
480 recovery.resetAt === resetAt &&
481 snoozedUntil != null &&
482 Date.parse(snoozedUntil) === Date.parse(resetAt)
483 );
485​
486export function threadRuntimeHasInterruptibleRun(
487 runtime: ThreadStructureRuntime | null | undefined,
488): boolean {
489 return (
490 threadRuntimeIsActive(runtime) &&
491 runtime?.activeRunId !== null &&
492 runtime?.activeRunId !== undefined
493 );
495​
496type BackgroundWorkKind = OrchestrationV2PendingBackgroundTask["kind"];
497​
498const BACKGROUND_WORK_KINDS: Record<
499 BackgroundWorkKind,
500 { readonly order: number; readonly singular: string; readonly plural: string }
501> = {
502 subagent: { order: 0, singular: "subagent", plural: "subagents" },
503 command: { order: 1, singular: "command", plural: "commands" },
504 monitor: { order: 2, singular: "monitor", plural: "monitors" },
505 background_task: { order: 3, singular: "background task", plural: "background tasks" },
506};
507​
508export interface RunStopPresentation {
509 /** `unresponsive`: the provider did not confirm the stop in time, so Force stop is offered. */
510 readonly state: "stopping" | "unresponsive";
511 readonly runId: ThreadStructure["runs"][number]["id"];
512 readonly requestedAt: string;
513 /** The run already ended; the Stop targeted its background work. */
514 readonly background: boolean;
516​
517/**
518 * A Stop the provider has not finished yet. It stays `stopping` while the agent winds down,
519 * since some providers confirm the request before the turn actually ends. On a run that
520 * already ended, only an unanswered Stop of its background work counts.
521 */
522export function presentRunStop(
523 projection: Pick<ThreadStructure, "runs" | "turnItems">,
524): RunStopPresentation | null {
525 let active: ThreadStructure["runs"][number] | null = null;
526 let latest: ThreadStructure["runs"][number] | null = null;
527 for (const candidate of projection.runs) {
528 // Queued follow-ups never ran, so they hold no Stop of their own.
529 if (candidate.status !== "queued" && candidate.ordinal > (latest?.ordinal ?? -1))
530 latest = candidate;
531 if (
532 INTERRUPTIBLE_RUN_STATUSES.has(candidate.status) &&
533 candidate.ordinal > (active?.ordinal ?? -1)
534 )
535 active = candidate;
536 }
537 const run = active ?? latest;
538 if (run === null) return null;
539 let request:
540 | Extract<ThreadStructure["turnItems"][number], { type: "run_interrupt_request" }>
541 | undefined;
542 for (const item of projection.turnItems) {
543 if (item.runId !== run.id) continue;
544 if (item.type === "run_interrupt_result") return null;
545 if (item.type === "run_interrupt_request") request = item;
546 }
547 if (request === undefined || (active === null && request.unresponsiveAt === undefined))
548 return null;
549 return {
550 state: request.unresponsiveAt === undefined ? "stopping" : "unresponsive",
551 runId: run.id,
552 requestedAt: DateTime.formatIso(request.startedAt ?? request.updatedAt),
553 background: active === null,
554 };
556​
557export interface PendingBackgroundWorkItem {
558 readonly taskId: string;
559 readonly kind: BackgroundWorkKind;
560 readonly label: string;
561 readonly childThreadId: ThreadId | undefined;
563​
564export interface PendingBackgroundWorkPresentation {
565 readonly title: string;
566 readonly items: ReadonlyArray<PendingBackgroundWorkItem>;
567 readonly waiting: boolean;
569​
570function joinWithAnd(parts: ReadonlyArray<string>): string {
571 if (parts.length <= 1) return parts.join("");
572 return `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
574​
575// The Claude adapter forces a stuck query closed after 10 seconds.
576export const BACKGROUND_WORK_STOP_RETRY_AFTER_MS = 10_000;
577​
578export interface BackgroundWorkStopRequest {
579 readonly threadKey: string;
580 readonly taskIds: ReadonlyArray<string>;
581 readonly retryAt: number | null;
583​
584/** Acceptance does not end the tasks, so an accepted Stop stays pending while they are listed. */
585export function isBackgroundWorkStopPending(
586 request: BackgroundWorkStopRequest | null,
587 threadKey: string,
588 tasks: ReadonlyArray<Pick<OrchestrationV2PendingBackgroundTask, "taskId">>,
589 retryExpired: boolean,
590): boolean {
591 if (request === null || request.threadKey !== threadKey || retryExpired) return false;
592 return request.retryAt === null || tasks.some((task) => request.taskIds.includes(task.taskId));
594​
595export function presentPendingBackgroundWork(
596 tasks: ReadonlyArray<OrchestrationV2PendingBackgroundTask>,
597): PendingBackgroundWorkPresentation | null {
598 if (tasks.length === 0) return null;
599 const waiting = backgroundWorkHoldsCompletion(tasks);
600 const items = tasks
601 .map((task): PendingBackgroundWorkItem => {
602 const description = task.description?.trim();
603 const label =
604 description === undefined
605 ? undefined
606 : task.kind === "subagent"
607 ? formatSubagentDisplayTitle(description).trim()
608 : task.kind === "command" || task.kind === "monitor"
609 ? commandDisplayText(description)
610 : description;
611 return {
612 taskId: task.taskId,
613 kind: task.kind,
614 label:
615 label === undefined || label.length === 0
616 ? BACKGROUND_WORK_KINDS[task.kind].singular
617 : label,
618 childThreadId: task.kind === "subagent" ? task.childThreadId : undefined,
619 };
620 })
621 // `map` returned a new array; Hermes has no `toSorted`.
622 .sort(
623 (left, right) =>
624 BACKGROUND_WORK_KINDS[left.kind].order - BACKGROUND_WORK_KINDS[right.kind].order,
625 );
626 const [only] = items;
627 if (items.length === 1 && only !== undefined) {
628 const noun = BACKGROUND_WORK_KINDS[only.kind].singular;
629 const named = only.label !== noun;
630 const title = named
631 ? `${waiting ? "Waiting" : "Running"} · ${only.label}`
632 : waiting
633 ? `Waiting on a ${noun}`
634 : `Running a ${noun}`;
635 return { title, items, waiting };
636 }
637 const counts = new Map<BackgroundWorkKind, number>();
638 for (const item of items) counts.set(item.kind, (counts.get(item.kind) ?? 0) + 1);
639 const groups = Array.from(counts, ([kind, count]) => {
640 const { singular, plural } = BACKGROUND_WORK_KINDS[kind];
641 return `${count} ${count === 1 ? singular : plural}`;
642 });
643 return { title: `${waiting ? "Waiting on" : "Running"} ${joinWithAnd(groups)}`, items, waiting };
645​
646export interface ProviderGoalPresentation {
647 readonly title: string;
648 readonly objective: string;
649 readonly usage: string | null;
650 readonly canResume: boolean;
652​
653const PROVIDER_GOAL_TITLES: Record<OrchestrationV2ProviderGoal["status"], string> = {
654 active: "Pursuing goal",
655 paused: "Goal paused",
656 blocked: "Goal blocked",
657 usage_limited: "Goal hit a usage limit",
658 budget_limited: "Goal reached its token budget",
659 complete: "Goal complete",
660};
661​
662function formatGoalTokens(tokens: number): string {
663 if (tokens < 1_000) return `${tokens}`;
664 const thousands = Math.round(tokens / 1_000);
665 if (thousands < 1_000) return `${thousands}k`;
666 return `${(tokens / 1_000_000).toFixed(1).replace(/\.0$/, "")}m`;
668​
669export function presentProviderGoal(
670 goal: OrchestrationV2ProviderGoal,
671 working: boolean,
672): ProviderGoalPresentation {
673 const usage: Array<string> = [];
674 if (goal.tokensUsed !== undefined && goal.tokensUsed > 0) {
675 usage.push(
676 goal.tokenBudget == null
677 ? `${formatGoalTokens(goal.tokensUsed)} tokens`
678 : `${formatGoalTokens(goal.tokensUsed)} / ${formatGoalTokens(goal.tokenBudget)} tokens`,
679 );
680 }
681 if (goal.timeUsedSeconds !== undefined && goal.timeUsedSeconds >= 60) {
682 usage.push(formatDuration(goal.timeUsedSeconds * 1_000));
683 }
684 if (goal.checks !== undefined && goal.checks > 0) {
685 usage.push(`${goal.checks} ${goal.checks === 1 ? "check" : "checks"}`);
686 }
687 return {
688 title: goal.status === "active" && !working ? "Goal set" : PROVIDER_GOAL_TITLES[goal.status],
689 objective: goal.objective,
690 usage: usage.length === 0 ? null : usage.join(" · "),
691 canResume: goal.status !== "active" && goal.status !== "complete",
692 };
694​
695export function notificationChildThreadId(
696 source: OrchestrationV2NotificationSource,
697): ThreadId | undefined {
698 switch (source.kind) {
699 case "subagent":
700 case "delegated_task":
701 return source.childThreadId;
702 case "command":
703 case "monitor":
704 case "background_task":
705 case "system":
706 return undefined;
707 default:
708 source satisfies never;
709 return undefined;
710 }