Open History to see the last commit
1import { groupBy } from "effect/Array";
2import * as DateTime from "effect/DateTime";
3import type {
4 ModelSelection,
5 OrchestrationV2AppThread,
6 OrchestrationV2Subagent,
7 OrchestrationV2SubagentWorkflow,
8 OrchestrationV2TurnItem,
9} from "@t3tools/contracts";
10import { isOrchestrationV2WorkActive, isProviderNativeSubagentThread } from "@t3tools/contracts";
11
12export type RuntimeSubagentStatus =
13 | "pending"
14 | "running"
15 | "waiting"
16 | "idle"
17 | "completed"
18 | "failed"
19 | "cancelled"
20 | "interrupted";
21
22export interface SubagentUsage {
23 readonly totalTokens: number;
24 readonly inputTokens?: number;
25 readonly cachedInputTokens?: number;
26 readonly outputTokens?: number;
27 readonly reasoningOutputTokens?: number;
28 readonly toolUses?: number;
29 readonly durationMs?: number;
30}
31
32export interface SubagentActivityEntry {
33 readonly at: string;
34 readonly summary: string;
35}
36
37export interface SubagentWorkflowPhase {
38 readonly index: number;
39 readonly title: string;
40}
41
42export interface SubagentRunHandles {
43 readonly runId?: string | undefined;
44 readonly scriptPath?: string | undefined;
45 readonly transcriptDir?: string | undefined;
46 readonly sessionUrl?: string | undefined;
47}
48
49export interface RuntimeSubagent {
50 readonly id: string;
51 readonly kind: "subagent" | "subagent_batch" | "workflow" | "workflow_agent";
52 readonly title: string;
53 readonly role: string | null;
54 readonly model: string | null;
55 readonly effort: string | null;
56 readonly speed?: string | undefined;
57 readonly status: RuntimeSubagentStatus;
58 readonly activationCount: number;
59 readonly usage: SubagentUsage | null;
60 readonly progress: string | null;
61 readonly lastToolName: string | null;
62 readonly result: string | null;
63 readonly error: string | null;
64 readonly outputFile: string | null;
65 readonly parentAgentId: string | null;
66 readonly agentIndex: number | null;
67 readonly phaseIndex: number | null;
68 readonly phaseTitle: string | null;
69 readonly attempt: number | null;
70 readonly workflowName: string | null;
71 readonly phases: ReadonlyArray<SubagentWorkflowPhase>;
72 readonly runHandles: SubagentRunHandles | null;
73 readonly childThreadId: string | null;
74 readonly recentActivity: ReadonlyArray<SubagentActivityEntry>;
75 readonly firstSeenAt: string;
76 readonly startedAt: string | null;
77 readonly completedAt: string | null;
78 readonly updatedAt: string;
79}
80
81const TERMINAL_STATUSES: ReadonlySet<RuntimeSubagentStatus> = new Set([
82 "completed",
83 "failed",
84 "cancelled",
85 "interrupted",
86]);
87
88export function isTerminalSubagentStatus(status: RuntimeSubagentStatus): boolean {
89 return TERMINAL_STATUSES.has(status);
90}
91
92export function isActiveSubagentStatus(status: RuntimeSubagentStatus): boolean {
93 return isOrchestrationV2WorkActive(status);
94}
95
96export function isLiveSubagentTurnItem(item: OrchestrationV2TurnItem): boolean {
97 return item.type === "subagent" && isOrchestrationV2WorkActive(item.status);
98}
99
100export interface SubagentWorkflowGroup {
101 readonly workflow: RuntimeSubagent;
102 readonly phases: ReadonlyArray<{
103 readonly index: number;
104 readonly title: string;
105 readonly members: ReadonlyArray<RuntimeSubagent>;
106 readonly state: "pending" | "running" | "done";
107 readonly settledCount: number;
108 }>;
109 readonly unphasedMembers: ReadonlyArray<RuntimeSubagent>;
110}
111
112function workflowUsage(source: {
113 readonly totalTokens?: number | undefined;
114 readonly toolCalls?: number | undefined;
115 readonly durationMs?: number | undefined;
116}): SubagentUsage | null {
117 if (source.totalTokens === undefined) return null;
118 return {
119 totalTokens: source.totalTokens,
120 ...(source.toolCalls === undefined ? {} : { toolUses: source.toolCalls }),
121 ...(source.durationMs === undefined ? {} : { durationMs: source.durationMs }),
122 };
123}
124
125function isoFromEpochMillis(value: number | undefined): string | null {
126 return value === undefined ? null : DateTime.formatIso(DateTime.makeUnsafe(value));
127}
128
129function workflowMembersToRuntime(
130 coordinator: RuntimeSubagent,
131 workflow: OrchestrationV2SubagentWorkflow,
132): ReadonlyArray<RuntimeSubagent> {
133 return workflow.agents.map((agent) => {
134 const status =
135 (agent.state === "queued" || agent.state === "running") &&
136 isTerminalSubagentStatus(coordinator.status)
137 ? coordinator.status
138 : agent.state === "queued"
139 ? "pending"
140 : agent.state;
141 const failed = status === "failed";
142 const startedAt = isoFromEpochMillis(agent.startedAt);
143 const completedAt = isTerminalSubagentStatus(status)
144 ? (isoFromEpochMillis(agent.completedAt) ??
145 (agent.startedAt !== undefined && agent.durationMs !== undefined
146 ? isoFromEpochMillis(agent.startedAt + agent.durationMs)
147 : coordinator.completedAt))
148 : null;
149 return {
150 ...coordinator,
151 id: `${coordinator.id}:agent:${agent.index}`,
152 kind: "workflow_agent" as const,
153 title: agent.label,
154 model: agent.model ?? null,
155 effort: agent.effort ?? null,
156 ...(agent.speed === undefined ? {} : { speed: agent.speed }),
157 status,
158 activationCount: agent.attempt ?? 1,
159 usage: workflowUsage(agent),
160 progress: agent.prompt ?? null,
161 result: failed ? null : (agent.result ?? null),
162 error: failed ? (agent.result ?? null) : null,
163 parentAgentId: coordinator.id,
164 agentIndex: agent.index,
165 phaseIndex: agent.phaseIndex ?? null,
166 phaseTitle: agent.phaseTitle ?? null,
167 attempt: agent.attempt ?? null,
168 phases: [],
169 childThreadId: agent.childThreadId ?? null,
170 recentActivity: [],
171 firstSeenAt: isoFromEpochMillis(agent.queuedAt) ?? startedAt ?? coordinator.firstSeenAt,
172 startedAt,
173 completedAt,
174 updatedAt: completedAt ?? startedAt ?? coordinator.firstSeenAt,
175 } satisfies RuntimeSubagent;
176 });
177}
178
179function promptTitle(prompt: string): string {
180 if (prompt.length <= 80) return prompt;
181 let end = 77;
182 const previous = prompt.charCodeAt(end - 1);
183 const next = prompt.charCodeAt(end);
184 if (previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
185 end -= 1;
186 }
187 return `${prompt.slice(0, end)}...`;
188}
189
190export function workflowMemberModelSelection(
191 member: Pick<RuntimeSubagent, "model" | "effort" | "speed">,
192 instanceId: ModelSelection["instanceId"],
193): ModelSelection | undefined {
194 if (!member.model) return undefined;
195 return {
196 instanceId,
197 model: member.model,
198 options: [
199 ...(member.effort ? [{ id: "effort", value: member.effort }] : []),
200 ...(member.speed ? [{ id: "fastMode", value: member.speed === "fast" }] : []),
201 ],
202 };
203}
204
205export function projectedSubagentsToRuntime(
206 subagents: ReadonlyArray<{
207 readonly id: string;
208 readonly title: string | null;
209 readonly prompt: string;
210 readonly model: string | null;
211 readonly status: OrchestrationV2Subagent["status"];
212 readonly progress?: string | undefined;
213 readonly childThreadId?: string | null | undefined;
214 readonly workflow?: OrchestrationV2SubagentWorkflow | undefined;
215 readonly result: string | null;
216 readonly startedAt: DateTime.Utc | null;
217 readonly completedAt: DateTime.Utc | null;
218 readonly updatedAt: DateTime.Utc;
219 }>,
220): ReadonlyArray<RuntimeSubagent> {
221 return subagents.flatMap((subagent) => {
222 const updatedAt = DateTime.formatIso(subagent.updatedAt);
223 const startedAt = subagent.startedAt === null ? null : DateTime.formatIso(subagent.startedAt);
224 const firstSeenAt = startedAt ?? updatedAt;
225 const { workflow } = subagent;
226 const runHandles = workflow?.runHandles ?? null;
227 const coordinator = {
228 id: subagent.id,
229 kind: workflow === undefined ? "subagent" : "workflow",
230 title: subagent.title ?? promptTitle(subagent.prompt),
231 role: null,
232 model: subagent.model,
233 effort: null,
234 status: subagent.status,
235 activationCount: 1,
236 usage: workflow === undefined ? null : workflowUsage(workflow),
237 progress: subagent.progress ?? null,
238 lastToolName: null,
239 result: subagent.result,
240 error: subagent.status === "failed" ? (subagent.result ?? null) : null,
241 outputFile: null,
242 parentAgentId: null,
243 agentIndex: null,
244 phaseIndex: null,
245 phaseTitle: null,
246 attempt: null,
247 workflowName: workflow?.name ?? null,
248 phases: workflow?.phases ?? [],
249 runHandles,
250 childThreadId: subagent.childThreadId ?? null,
251 recentActivity: [],
252 firstSeenAt,
253 startedAt,
254 completedAt: subagent.completedAt === null ? null : DateTime.formatIso(subagent.completedAt),
255 updatedAt,
256 } satisfies RuntimeSubagent;
257 return workflow === undefined
258 ? [coordinator]
259 : [coordinator, ...workflowMembersToRuntime(coordinator, workflow)];
260 });
261}
262
263export function deriveWorkflowGroups(
264 agents: ReadonlyArray<RuntimeSubagent>,
265): ReadonlyArray<SubagentWorkflowGroup> {
266 const workflows = agents
267 .filter((agent) => agent.kind === "workflow")
268 .sort((a, b) => a.firstSeenAt.localeCompare(b.firstSeenAt) || a.id.localeCompare(b.id));
269 const members = groupBy(
270 agents.filter((agent) => agent.kind !== "workflow" && agent.parentAgentId !== null),
271 (agent) => agent.parentAgentId!,
272 );
273
274 return workflows.map((workflow) => {
275 const workflowMembers = Object.hasOwn(members, workflow.id) ? members[workflow.id]! : [];
276 // Union, not either/or: the declared plan lags the members, because the
277 // provider only admits a phase once something in it starts. Members seed
278 // the map; the declared titles then overwrite whatever they guessed.
279 const phaseTitles = new Map<number, string>();
280 for (const member of workflowMembers) {
281 if (member.phaseIndex === null) continue;
282 phaseTitles.set(
283 member.phaseIndex,
284 member.phaseTitle ?? phaseTitles.get(member.phaseIndex) ?? `Phase ${member.phaseIndex + 1}`,
285 );
286 }
287 for (const phase of workflow.phases) phaseTitles.set(phase.index, phase.title);
288 const knownPhases = Array.from(phaseTitles.entries())
289 .map(([index, title]) => ({ index, title }))
290 .sort((a, b) => a.index - b.index);
291
292 const phases = knownPhases.map((phase) => {
293 const phaseMembers = workflowMembers
294 .filter((member) => member.phaseIndex === phase.index)
295 .sort((a, b) => (a.agentIndex ?? 0) - (b.agentIndex ?? 0));
296 const hasActiveMember = phaseMembers.some(
297 // Idle members count as active for phase-liveness: a resumable Codex
298 // member has not finished the phase.
299 (member) => isActiveSubagentStatus(member.status) || member.status === "idle",
300 );
301 const settledCount = phaseMembers.filter((member) =>
302 isTerminalSubagentStatus(member.status),
303 ).length;
304 const state: "pending" | "running" | "done" =
305 phaseMembers.length === 0 ? "pending" : hasActiveMember ? "running" : "done";
306 return {
307 index: phase.index,
308 title: phase.title,
309 members: phaseMembers,
310 state,
311 settledCount,
312 };
313 });
314
315 const unphasedMembers = workflowMembers
316 .filter((member) => member.phaseIndex === null)
317 .sort((a, b) => (a.agentIndex ?? 0) - (b.agentIndex ?? 0));
318
319 return { workflow, phases, unphasedMembers };
320 });
321}
322
323// Workflow scripts open with their `meta` export, after any leading comments.
324const WORKFLOW_SCRIPT_START = /^\s*(?:(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)\s*)*export\s+const\s+meta\b/;
325
326/**
327 * Whether a user message is the script a provider-native workflow runs. Servers mark every
328 * subagent prompt either way; only threads recorded before the mark fall back to the text.
329 */
330export function isWorkflowScriptMessage(message: {
331 readonly role: string;
332 readonly text: string;
333 readonly workflowScript?: boolean | undefined;
334}): boolean {
335 return (
336 message.role === "user" && (message.workflowScript ?? WORKFLOW_SCRIPT_START.test(message.text))
337 );
338}
339
340type ProjectedSubagent = Parameters<typeof projectedSubagentsToRuntime>[0][number] &
341 Pick<OrchestrationV2Subagent, "driver" | "providerInstanceId">;
342
343export interface WorkflowOpening {
344 readonly messageId: string;
345 readonly script: string;
346 readonly name: string | null;
347 readonly description: string | null;
348 readonly driver: OrchestrationV2Subagent["driver"] | null;
349 readonly providerInstanceId: OrchestrationV2Subagent["providerInstanceId"] | null;
350 /** Null until the parent thread's coordinator row is loaded. */
351 readonly group: SubagentWorkflowGroup | null;
352}
353
354/**
355 * The opening of a workflow coordinator's thread: its first user message is the workflow script,
356 * shown as the workflow's plan. The coordinator row lives on the parent thread, so
357 * `parentSubagents` is the parent projection's subagents, or null when it is not loaded.
358 */
359export function resolveWorkflowOpening(input: {
360 readonly thread: Pick<OrchestrationV2AppThread, "id" | "lineage" | "creationSource">;
361 readonly messages: ReadonlyArray<{
362 readonly id: string;
363 readonly role: string;
364 readonly text: string;
365 readonly workflowScript?: boolean | undefined;
366 }>;
367 readonly parentSubagents: ReadonlyArray<ProjectedSubagent> | null;
368}): WorkflowOpening | null {
369 if (!isProviderNativeSubagentThread(input.thread)) return null;
370 const first = input.messages.find((message) => message.role === "user");
371 if (first === undefined) return null;
372 const coordinator = input.parentSubagents?.find(
373 (subagent) => subagent.childThreadId === input.thread.id,
374 );
375 if (first.workflowScript === false) return null;
376 if (coordinator?.workflow === undefined && !isWorkflowScriptMessage(first)) return null;
377 return {
378 messageId: first.id,
379 script: first.text,
380 name: coordinator?.workflow?.name ?? null,
381 description: coordinator?.title ?? null,
382 driver: coordinator?.driver ?? null,
383 providerInstanceId: coordinator?.providerInstanceId ?? null,
384 group:
385 coordinator?.workflow === undefined
386 ? null
387 : (deriveWorkflowGroups(projectedSubagentsToRuntime([coordinator]))[0] ?? null),
388 };
389}
390
391/**
392 * The parent thread whose subagent rows describe this thread's workflow, or null when the thread
393 * does not open with a workflow script. Gates loading the parent so ordinary subagent threads
394 * never subscribe to their parent's detail.
395 */
396export function workflowOpeningParentThreadId(input: {
397 readonly thread: Pick<OrchestrationV2AppThread, "lineage" | "creationSource">;
398 readonly messages: ReadonlyArray<Parameters<typeof isWorkflowScriptMessage>[0]>;
399}): OrchestrationV2AppThread["id"] | null {
400 if (!isProviderNativeSubagentThread(input.thread)) return null;
401 const first = input.messages.find((message) => message.role === "user");
402 return first !== undefined && isWorkflowScriptMessage(first)
403 ? input.thread.lineage.parentThreadId
404 : null;
405}
406
407/** The opening block's heading: the workflow, what it does, and how its agents stand. */
408export function workflowOpeningHeadline(opening: WorkflowOpening): {
409 readonly title: string;
410 readonly description: string | null;
411 readonly agents: string | null;
412} {
413 const title = opening.name ?? opening.description ?? "Workflow";
414 const members =
415 opening.group === null
416 ? []
417 : [
418 ...opening.group.phases.flatMap((phase) => phase.members),
419 ...opening.group.unphasedMembers,
420 ];
421 const counts = new Map<string, number>();
422 for (const member of members) {
423 const key = isTerminalSubagentStatus(member.status)
424 ? member.status === "completed"
425 ? "done"
426 : member.status === "failed"
427 ? "failed"
428 : "stopped"
429 : "working";
430 counts.set(key, (counts.get(key) ?? 0) + 1);
431 }
432 const agents =
433 members.length === 0
434 ? null
435 : `${members.length} ${members.length === 1 ? "agent" : "agents"} · ${[
436 "working",
437 "done",
438 "failed",
439 "stopped",
440 ]
441 .filter((key) => counts.has(key))
442 .map((key) => `${counts.get(key)} ${key}`)
443 .join(" · ")}`;
444 return {
445 title,
446 description:
447 opening.description !== null && opening.description !== title ? opening.description : null,
448 agents,
449 };
450}