1import type {
2 OrchestrationV2ConversationMessage,
3 OrchestrationV2Notification,
4 OrchestrationV2NotificationSource,
5 OrchestrationV2Subagent,
6 OrchestrationV2TurnItem,
7 ThreadId,
8} from "@t3tools/contracts";
9
10type NotificationOutcome = OrchestrationV2Notification["outcome"];
11
12export type BackgroundWork = {
13 readonly label?: string | undefined;
14} & (
15 | { readonly kind: "subagent"; readonly childThreadId?: ThreadId | undefined }
16 | { readonly kind: "command"; readonly exitCode?: number | undefined }
17 | { readonly kind: "monitor" }
18 | { readonly kind: "background_task" }
19);
20
21export type BackgroundWorkReport = BackgroundWork & { readonly outcome: NotificationOutcome };
22
23const LABEL_MAX_LENGTH = 80;
24
25export function reportLabel(label: string | undefined): string | undefined {
26 const firstLine = label?.trim().split("\n")[0]?.trim();
27 if (firstLine === undefined || firstLine.length === 0) return undefined;
28 if (firstLine.length <= LABEL_MAX_LENGTH) return firstLine;
29 let end = LABEL_MAX_LENGTH - 1;
30 const previous = firstLine.charCodeAt(end - 1);
31 const next = firstLine.charCodeAt(end);
32 if (previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
33 end -= 1;
34 }
35 return `${firstLine.slice(0, end)}…`;
36}
37
38const KIND_NOUN: Record<BackgroundWork["kind"], readonly [string, string]> = {
39 subagent: ["Subagent", "subagents"],
40 command: ["Command", "commands"],
41 monitor: ["Monitor", "monitors"],
42 background_task: ["Background task", "background tasks"],
43};
44
45function outcomeVerb(kind: BackgroundWork["kind"], outcome: NotificationOutcome) {
46 switch (outcome) {
47 case "failed":
48 return "failed";
49 case "cancelled":
50 return "was stopped";
51 case "updated":
52 return kind === "monitor" ? "reported new output" : "updated";
53 case "completed":
54 case "unknown":
55 return "finished";
56 }
57}
58
59function combinedOutcome(outcomes: ReadonlyArray<NotificationOutcome>): NotificationOutcome {
60 if (outcomes.includes("failed")) return "failed";
61 if (outcomes.includes("cancelled")) return "cancelled";
62 if (outcomes.length > 0 && outcomes.every((outcome) => outcome === "completed")) {
63 return "completed";
64 }
65 return outcomes.includes("updated") ? "updated" : "unknown";
66}
67
68function exitSuffix(report: BackgroundWorkReport): string {
69 return report.kind === "command" && report.exitCode !== undefined
70 ? ` (exit ${report.exitCode})`
71 : "";
72}
73
74function namedReport(report: BackgroundWorkReport, capitalize: boolean): string {
75 const noun = KIND_NOUN[report.kind][0];
76 const label = reportLabel(report.label);
77 const named = `${capitalize ? noun : noun.toLowerCase()}${label === undefined ? "" : ` "${label}"`}`;
78 return `${named}${exitSuffix(report)}`;
79}
80
81function joinNames(names: ReadonlyArray<string>): string {
82 if (names.length <= 1) return names.join("");
83 return `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
84}
85
86function reportsSummary(
87 reports: readonly [BackgroundWorkReport, ...ReadonlyArray<BackgroundWorkReport>],
88 outcome: NotificationOutcome,
89): string {
90 const [first] = reports;
91 if (reports.length === 1) {
92 const label = reportLabel(first.label);
93 const noun = KIND_NOUN[first.kind][0];
94 return `${noun}${label === undefined ? "" : ` "${label}"`} ${outcomeVerb(first.kind, first.outcome)}${exitSuffix(first)}`;
95 }
96 const verb =
97 outcome === "failed"
98 ? "failed"
99 : outcome === "cancelled"
100 ? "were stopped"
101 : outcome === "updated"
102 ? "updated"
103 : "finished";
104 if (reports.length > 3 || reports.every((report) => reportLabel(report.label) === undefined)) {
105 const kinds = new Set(reports.map((report) => report.kind));
106 const noun = kinds.size === 1 ? KIND_NOUN[first.kind][1] : "background tasks";
107 return `${reports.length} ${noun} ${verb}`;
108 }
109 return `${joinNames(reports.map((report, index) => namedReport(report, index === 0)))} ${verb}`;
110}
111
112function reportsSource(
113 reports: readonly [BackgroundWorkReport, ...ReadonlyArray<BackgroundWorkReport>],
114): OrchestrationV2NotificationSource {
115 const [first, ...rest] = reports;
116 if (rest.some((report) => report.kind !== first.kind)) return { kind: "background_task" };
117 switch (first.kind) {
118 case "subagent":
119 return rest.length === 0 && first.childThreadId !== undefined
120 ? { kind: "subagent", childThreadId: first.childThreadId }
121 : { kind: "subagent" };
122 case "command":
123 case "monitor":
124 case "background_task":
125 return { kind: first.kind };
126 }
127}
128
129export function backgroundWorkNotification(
130 reports: readonly [BackgroundWorkReport, ...ReadonlyArray<BackgroundWorkReport>],
131): OrchestrationV2Notification;
132export function backgroundWorkNotification(
133 reports: ReadonlyArray<BackgroundWorkReport>,
134): OrchestrationV2Notification | null;
135export function backgroundWorkNotification(
136 reports: ReadonlyArray<BackgroundWorkReport>,
137): OrchestrationV2Notification | null {
138 const [first, ...rest] = reports;
139 if (first === undefined) return null;
140 const outcome = combinedOutcome(reports.map((report) => report.outcome));
141 return {
142 source: reportsSource([first, ...rest]),
143 outcome,
144 summary: reportsSummary([first, ...rest], outcome),
145 };
146}
147
148function delegatedTaskReport(
149 task: OrchestrationV2Subagent | undefined,
150): Extract<BackgroundWorkReport, { readonly kind: "subagent" }> {
151 const outcome: NotificationOutcome =
152 task?.status === "failed"
153 ? "failed"
154 : task?.status === "cancelled" || task?.status === "interrupted"
155 ? "cancelled"
156 : task?.status === "completed"
157 ? "completed"
158 : "unknown";
159 return {
160 kind: "subagent",
161 label: task?.title?.trim() || task?.prompt,
162 outcome,
163 ...(task?.childThreadId == null ? {} : { childThreadId: task.childThreadId }),
164 };
165}
166
167function delegatedCompletionNotification(
168 completion: NonNullable<OrchestrationV2ConversationMessage["delegatedCompletion"]>,
169 tasks: ReadonlyArray<OrchestrationV2Subagent>,
170): OrchestrationV2Notification {
171 const taskIds = completion.taskIds;
172 const reports = taskIds.map((taskId) =>
173 delegatedTaskReport(tasks.find((task) => task.id === taskId)),
174 );
175 const delegatedByRun = tasks.filter(
176 (task) => task.origin === "app_owned" && task.runId === completion.parentRunId,
177 ).length;
178 const outcome = combinedOutcome(reports.map((report) => report.outcome));
179 const verb = outcome === "failed" ? "failed" : outcome === "cancelled" ? "stopped" : "finished";
180 const [only] = reports;
181 if (reports.length === 1 && only !== undefined) {
182 const label = reportLabel(only.label);
183 return {
184 source: {
185 kind: "delegated_task",
186 taskIds,
187 ...(only.childThreadId === undefined ? {} : { childThreadId: only.childThreadId }),
188 },
189 outcome,
190 summary: `Delegated task${label === undefined ? "" : ` "${label}"`} ${verb}`,
191 };
192 }
193 const labels = reports.flatMap((report) => reportLabel(report.label) ?? []);
194 const count =
195 delegatedByRun > taskIds.length
196 ? `${taskIds.length} of ${delegatedByRun}`
197 : `${taskIds.length}`;
198 return {
199 source: { kind: "delegated_task", taskIds },
200 outcome,
201 summary: `${count} delegated tasks ${verb}${labels.length === 0 ? "" : `: ${labels.join(", ")}`}`,
202 };
203}
204
205export function notificationTurnItem(
206 item: OrchestrationV2TurnItem,
207 message: Pick<OrchestrationV2ConversationMessage, "notification" | "delegatedCompletion">,
208 tasks: ReadonlyArray<OrchestrationV2Subagent>,
209): OrchestrationV2TurnItem {
210 if (item.type !== "user_message") return item;
211 const notification =
212 message.delegatedCompletion === undefined
213 ? message.notification
214 : delegatedCompletionNotification(message.delegatedCompletion, tasks);
215 if (notification === undefined) return item;
216 const {
217 type: _type,
218 messageId: _messageId,
219 inputIntent: _inputIntent,
220 text: _text,
221 attachments: _attachments,
222 createdBy: _createdBy,
223 creationSource: _creationSource,
224 scheduledTaskId: _scheduledTaskId,
225 modelSwitch: _modelSwitch,
226 ...base
227 } = item;
228 return { ...base, type: "notification", ...notification };
229}