| 1 | import type { |
| 2 | EnvironmentId, |
| 3 | OrchestrationV2ThreadShell, |
| 4 | Project, |
| 5 | ThreadId, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import * as DateTime from "effect/DateTime"; |
| 8 | |
| 9 | import { backgroundWorkHoldsCompletion } from "./orchestrationV2PendingBackgroundWork.ts"; |
| 10 | |
| 11 | export type AgentAwarenessPhase = |
| 12 | | "starting" |
| 13 | | "running" |
| 14 | | "waiting_for_approval" |
| 15 | | "waiting_for_input" |
| 16 | | "completed" |
| 17 | | "failed" |
| 18 | | "stale"; |
| 19 | |
| 20 | export 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 | |
| 33 | function buildAgentAwarenessDeepLink(input: { |
| 34 | readonly environmentId: EnvironmentId; |
| 35 | readonly threadId: ThreadId; |
| 36 | }): string { |
| 37 | return `/threads/${encodeURIComponent(input.environmentId)}/${encodeURIComponent(input.threadId)}`; |
| 38 | } |
| 39 | |
| 40 | export 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. */ |
| 58 | export 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 | |
| 87 | function 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 | |
| 122 | function 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 | } |