import { Effect, Schema } from "effect"; import { SessionGoalResponseSchema, type SessionGoal, type SessionGoalPatch, } from "@shared/agent/session-goal"; import type { SessionUsageTotals } from "@shared/agent/session-summary"; import { requestDecodedEffect } from "@/lib/api/request-json"; import { parseAgentTurnCommandResult, type AgentTurnCommandResult, } from "@/features/agent/messages"; import type { AgentQueueAction, AgentToolAccess } from "@shared/agent/agent-turn"; import type { AgentImageInput } from "@shared/agent/agent-image-input"; import type { BrowserBackend } from "@/features/agent/tools/types"; import type { ComposerPromptTemplateRef, ComposerSkillRef, } from "@/features/agent/composer-context"; import { RuntimeActivityPayloadSchema, RuntimeStatusSchema, RuntimeStatusResponseSchema, type RuntimeActivityPayload, type RuntimeContextUsage, type RuntimeSessionSummary, type RuntimeStatus, } from "@shared/agent/runtime-status"; export type { RuntimeActivityPayload, RuntimeContextUsage, RuntimeSessionSummary, RuntimeStatus }; export type { SessionUsageTotals }; const decodeActivityOption = Schema.decodeUnknownOption(RuntimeActivityPayloadSchema, { onExcessProperty: "preserve", }); const decodeStatusResponseOption = Schema.decodeUnknownOption(RuntimeStatusResponseSchema, { onExcessProperty: "preserve", }); function decodeRuntimeActivityPayload(raw: unknown): RuntimeActivityPayload | null { const option = decodeActivityOption(raw); return option._tag === "Some" ? option.value : null; } function decodeRuntimeStatusResponse(raw: unknown): RuntimeStatus | null { const option = decodeStatusResponseOption(raw); return option._tag === "Some" ? (option.value.status ?? null) : null; } const AbortSessionResponseSchema = Schema.Struct({ ok: Schema.Boolean, cleared: Schema.Struct({ steering: Schema.Array(Schema.String), followUp: Schema.Array(Schema.String), }), status: RuntimeStatusSchema, }); const decodeAbortSessionResponse = Schema.decodeUnknownOption(AbortSessionResponseSchema, { onExcessProperty: "preserve", }); export type AbortSessionResult = { steering: string[]; followUp: string[]; status?: RuntimeStatus; }; export function parseAbortSessionResult(input: unknown): AbortSessionResult { const decoded = decodeAbortSessionResponse(input); return decoded._tag === "Some" ? { steering: [...decoded.value.cleared.steering], followUp: [...decoded.value.cleared.followUp], status: decoded.value.status, } : { steering: [], followUp: [] }; } export function loadRuntimeStatus( sessionId: string, piSessionId?: string | null, ): Promise { const params = new URLSearchParams({ sessionId }); if (piSessionId) params.set("piSessionId", piSessionId); return Effect.runPromise( requestDecodedEffect( `/api/agent/runtime/status?${params.toString()}`, decodeRuntimeStatusResponse, { cache: "no-store" }, ).pipe(Effect.catch(() => Effect.succeed(null))), ); } export function abortSession( sessionId: string, piSessionId?: string | null, ): Promise { return Effect.runPromise( requestDecodedEffect("/api/agent/abort", parseAbortSessionResult, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, piSessionId }), }).pipe(Effect.catch(() => Effect.succeed({ steering: [], followUp: [] }))), ); } export function respondExtensionUi( sessionId: string, requestId: string, response: { value?: string; confirmed?: boolean; cancelled?: boolean }, ): Promise { return Effect.runPromise( requestDecodedEffect( "/api/agent/runtime/extension-ui", () => undefined, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, requestId, ...response }), }, "Extension response was rejected", ), ); } export type CanonicalSessionMeta = { title: string | null; modelId: string | null; startedAt: string | null; piSessionId: string | null; usage?: SessionUsageTotals | null; }; export type CanonicalSessionResult = { messages: import("@/features/agent/messages").ChatMessage[]; tokenStats?: import("@/features/agent/messages").TokenStats; // Byte-offset cursor to pass as `before` to load the previous (older) page, // or null when this page already reaches the start of the session log. cursor: number | null; // Session metadata from a head-scan; present on an initial tail load only. meta: CanonicalSessionMeta | null; }; // Default page size for the initial tail load — enough to fill a long scrollback // while keeping a giant log from being read/parsed whole. export const DEFAULT_SESSION_TAIL = 500; export type LoadCanonicalSessionOptions = { tail?: number; before?: number }; export function loadCanonicalSession( piSessionId: string, cwd: string, options: LoadCanonicalSessionOptions = {}, ): Promise { const params = new URLSearchParams({ cwd }); const tail = options.before === undefined ? (options.tail ?? DEFAULT_SESSION_TAIL) : undefined; if (tail !== undefined) params.set("tail", String(tail)); if (options.before !== undefined) params.set("before", String(options.before)); return Effect.runPromise( requestDecodedEffect( `/api/agent/sessions/${encodeURIComponent(piSessionId)}?${params.toString()}`, (input) => { const payload = input as Partial; return { messages: payload.messages ?? [], tokenStats: payload.tokenStats, cursor: payload.cursor ?? null, meta: payload.meta ?? null, }; }, { cache: "no-store" }, "Failed to load session", ), ); } export type CompactSessionArgs = { sessionId: string; modelId: string; thinkingLevel?: import("@shared/agent/agent-turn").AgentThinkingLevel; toolAccess?: AgentToolAccess; cwd?: string; piSessionId?: string | null; browserToolEnabled: boolean; browserSessionId?: string; browserBackend?: BrowserBackend; skills: ComposerSkillRef[]; promptTemplates?: ComposerPromptTemplateRef[]; }; export type CompactSessionResult = { status?: RuntimeStatus; }; export function compactSession(args: CompactSessionArgs): Promise { return Effect.runPromise( requestDecodedEffect( "/api/agent/compact", (input) => input as CompactSessionResult, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(args), }, "Compaction failed", ), ); } export type SubmitTurnArgs = { sessionId: string; modelId: string; thinkingLevel?: import("@shared/agent/agent-turn").AgentThinkingLevel; toolAccess: AgentToolAccess; message: string; images?: AgentImageInput[]; cwd?: string; piSessionId?: string | null; /** Control mode for steer/follow-up; omitted for a normal prompt. */ mode?: "steer" | "follow_up"; queueAction?: AgentQueueAction; queueReplacement?: string; browserToolEnabled: boolean; browserSessionId?: string; browserBackend?: BrowserBackend; skills: ComposerSkillRef[]; promptTemplates?: ComposerPromptTemplateRef[]; }; export function submitTurnCommand(args: SubmitTurnArgs): Promise { return Effect.runPromise( requestDecodedEffect( "/api/agent/turn", (input) => { const parsed = parseAgentTurnCommandResult(input); if (!parsed) throw new Error("Agent response was invalid"); if (parsed.outcome === "rejected") throw new Error(parsed.error || "Agent request was rejected"); return parsed; }, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(args), }, "Agent request failed", ), ); } export type RuntimeActivitySubscription = { close: () => void }; export function subscribeRuntimeActivity( onPayload: (payload: RuntimeActivityPayload) => void, ): RuntimeActivitySubscription { const source = new EventSource("/api/agent/runtime/activity"); source.onmessage = (event) => { let parsed: unknown; try { parsed = JSON.parse(event.data); } catch { return; } const payload = decodeRuntimeActivityPayload(parsed); if (!payload) return; onPayload(payload); }; return { close: () => source.close(), }; } const decodeSessionGoalResponseOption = Schema.decodeUnknownOption(SessionGoalResponseSchema, { onExcessProperty: "preserve", }); function decodeSessionGoal(raw: unknown): SessionGoal | null { if (!raw || typeof raw !== "object") return null; const option = decodeSessionGoalResponseOption(raw); return option._tag === "Some" ? option.value.goal : null; } const sessionGoalUrl = (piSessionId: string) => `/api/agent/goal?piSessionId=${encodeURIComponent(piSessionId)}`; export function loadSessionGoal(piSessionId: string): Promise { return Effect.runPromise( requestDecodedEffect(sessionGoalUrl(piSessionId), decodeSessionGoal, { cache: "no-store", }).pipe(Effect.catch(() => Effect.succeed(null))), ); } export function updateSessionGoal( piSessionId: string, patch: SessionGoalPatch, ): Promise { return Effect.runPromise( requestDecodedEffect( sessionGoalUrl(piSessionId), decodeSessionGoal, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), }, "Failed to update the goal", ), ); } export function clearSessionGoal(piSessionId: string): Promise { return Effect.runPromise( requestDecodedEffect( sessionGoalUrl(piSessionId), () => undefined, { method: "DELETE" }, "Failed to clear the goal", ), ); }