import type { EnvironmentId, OrchestrationProjectShell, OrchestrationThreadShell, ThreadId, } from "@t3tools/contracts"; export type AgentAwarenessPhase = | "starting" | "running" | "waiting_for_approval" | "waiting_for_input" | "completed" | "failed" | "stale"; export interface AgentAwarenessState { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly projectTitle: string; readonly threadTitle: string; readonly phase: AgentAwarenessPhase; readonly headline: string; readonly detail?: string; readonly modelTitle: string; readonly updatedAt: string; readonly deepLink: string; } export interface ProjectThreadAwarenessInput { readonly environmentId: EnvironmentId; readonly project: Pick; readonly thread: Pick< OrchestrationThreadShell, | "id" | "title" | "modelSelection" | "session" | "latestTurn" | "updatedAt" | "hasPendingApprovals" | "hasPendingUserInput" >; } function buildAgentAwarenessDeepLink(input: { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; }): string { return `/threads/${encodeURIComponent(input.environmentId)}/${encodeURIComponent(input.threadId)}`; } export function projectThreadAwareness( input: ProjectThreadAwarenessInput, ): AgentAwarenessState | null { const { environmentId, project, thread } = input; const phase = resolveThreadAwarenessPhase(thread); if (!phase) { return null; } const detail = detailForPhase(phase, thread); return { environmentId, threadId: thread.id, projectTitle: project.title, threadTitle: thread.title, phase, headline: headlineForPhase(phase), ...(detail === undefined ? {} : { detail }), modelTitle: thread.modelSelection.model, updatedAt: thread.updatedAt, deepLink: buildAgentAwarenessDeepLink({ environmentId, threadId: thread.id }), }; } function resolveThreadAwarenessPhase( thread: ProjectThreadAwarenessInput["thread"], ): AgentAwarenessPhase | null { if (thread.hasPendingApprovals) { return "waiting_for_approval"; } if (thread.hasPendingUserInput) { return "waiting_for_input"; } if (thread.session?.status === "error" || thread.latestTurn?.state === "error") { return "failed"; } if (thread.session?.status === "starting") { return "starting"; } if (thread.session?.status === "running" || thread.latestTurn?.state === "running") { return "running"; } if (thread.latestTurn?.state === "completed") { return "completed"; } // A turn that finished can still read as "interrupted" here: session // teardown settles still-running turns by session status, and that write // can race the turn.completed one. completedAt survives the race — a turn // that has a completion timestamp finished, whatever the state column says. // Without this, quick finish-then-teardown threads resolve to null // persistently and get tombstoned instead of published as completed. if (thread.latestTurn?.state === "interrupted" && thread.latestTurn.completedAt !== null) { return "completed"; } // Threads whose turns never produce a checkpoint (no code changes) have no // materialized latestTurn in the shell at all, and the session-set // projection clears latest_turn_id the moment the session settles. The // session status is then the only surviving completion signal: a live // session at "ready"/"idle" with nothing pending and nothing running means // the agent finished and is waiting for the next prompt — Done. if (thread.session?.status === "ready" || thread.session?.status === "idle") { return "completed"; } return null; } function headlineForPhase(phase: AgentAwarenessPhase): string { switch (phase) { case "starting": return "Starting agent"; case "running": return "Agent is working"; case "waiting_for_approval": return "Approval needed"; case "waiting_for_input": return "Waiting for input"; case "completed": return "Agent finished"; case "failed": return "Agent failed"; case "stale": return "Update delayed"; } } function detailForPhase( phase: AgentAwarenessPhase, thread: ProjectThreadAwarenessInput["thread"], ): string | undefined { if (phase === "failed") { return thread.session?.lastError ?? undefined; } if (phase === "completed") { return "Review the completed task."; } if (phase === "running" && thread.session?.providerName) { return `${thread.session.providerName} is active.`; } return undefined; }