1import type {
2 EnvironmentId,
3 OrchestrationV2ThreadShell,
4 Project,
5 ThreadId,
6} from "@t3tools/contracts";
7import * as DateTime from "effect/DateTime";
8
9import { backgroundWorkHoldsCompletion } from "./orchestrationV2PendingBackgroundWork.ts";
10
11export type AgentAwarenessPhase =
12 | "starting"
13 | "running"
14 | "waiting_for_approval"
15 | "waiting_for_input"
16 | "completed"
17 | "failed"
18 | "stale";
19
20export interface AgentAwarenessState {
21 readonly environmentId: EnvironmentId;
22 readonly threadId: ThreadId;
23 readonly projectTitle: string;
24 readonly threadTitle: string;
25 readonly phase: AgentAwarenessPhase;
26 readonly headline: string;
27 readonly detail?: string;
28 readonly modelTitle: string;
29 readonly updatedAt: string;
30 readonly deepLink: string;
31}
32
33function buildAgentAwarenessDeepLink(input: {
34 readonly environmentId: EnvironmentId;
35 readonly threadId: ThreadId;
36}): string {
37 return `/threads/${encodeURIComponent(input.environmentId)}/${encodeURIComponent(input.threadId)}`;
38}
39
40export interface ProjectThreadAwarenessV2Input {
41 readonly environmentId: EnvironmentId;
42 readonly project: Pick<Project, "title">;
43 readonly thread: Pick<
44 OrchestrationV2ThreadShell,
45 | "activityRunStatus"
46 | "id"
47 | "lineage"
48 | "modelSelection"
49 | "pendingBackgroundTasks"
50 | "pendingRuntimeRequest"
51 | "status"
52 | "title"
53 | "updatedAt"
54 >;
55}
56
57/** Build relay activity directly from the V2 shell projection. */
58export function projectThreadAwarenessV2(
59 input: ProjectThreadAwarenessV2Input,
60): AgentAwarenessState | null {
61 const { environmentId, project, thread } = input;
62 if (thread.lineage.relationshipToParent === "subagent") return null;
63 const phase = resolveThreadAwarenessPhaseV2(thread);
64 if (phase === null) {
65 return null;
66 }
67 const detail =
68 phase === "completed"
69 ? "Review the completed task."
70 : phase === "failed"
71 ? "The agent run failed."
72 : undefined;
73 return {
74 environmentId,
75 threadId: thread.id,
76 projectTitle: project.title,
77 threadTitle: thread.title,
78 phase,
79 headline: headlineForPhase(phase),
80 ...(detail === undefined ? {} : { detail }),
81 modelTitle: thread.modelSelection.model,
82 updatedAt: DateTime.formatIso(thread.updatedAt),
83 deepLink: buildAgentAwarenessDeepLink({ environmentId, threadId: thread.id }),
84 };
85}
86
87function resolveThreadAwarenessPhaseV2(
88 thread: ProjectThreadAwarenessV2Input["thread"],
89): AgentAwarenessPhase | null {
90 if (thread.pendingRuntimeRequest?.kind === "user_input") {
91 return "waiting_for_input";
92 }
93 if (
94 thread.pendingRuntimeRequest !== null &&
95 thread.pendingRuntimeRequest.kind !== "auth_refresh"
96 ) {
97 return "waiting_for_approval";
98 }
99 switch (thread.activityRunStatus ?? thread.status) {
100 case "preparing":
101 case "starting":
102 return "starting";
103 case "running":
104 case "waiting":
105 return "running";
106 case "completed":
107 // Work that will wake the agent keeps the run going; a dev server does not.
108 return backgroundWorkHoldsCompletion(thread.pendingBackgroundTasks ?? [])
109 ? "running"
110 : "completed";
111 case "failed":
112 return "failed";
113 case "idle":
114 case "queued":
115 case "interrupted":
116 case "cancelled":
117 case "rolled_back":
118 return null;
119 }
120}
121
122function headlineForPhase(phase: AgentAwarenessPhase): string {
123 switch (phase) {
124 case "starting":
125 return "Starting agent";
126 case "running":
127 return "Agent is working";
128 case "waiting_for_approval":
129 return "Approval needed";
130 case "waiting_for_input":
131 return "Waiting for input";
132 case "completed":
133 return "Agent finished";
134 case "failed":
135 return "Agent failed";
136 case "stale":
137 return "Update delayed";
138 }
139}