377 lines · 11.9 KB · TypeScript
RawDownload
import { Effect, Schema } from "effect";import {  SessionGoalResponseSchema,  type SessionGoal,  type SessionGoalPatch,} from "@shared/agent/session-goal";import { safeJson } from "@/features/agent/safe-json";import {  parseAgentTurnCommandResult,  type AgentTurnCommandResult,} from "@/features/agent/messages";import type {  AgentImageInput,  AgentQueueAction,  AgentToolAccess,} from "@/features/agent/contracts";import type { BrowserBackend } from "@/features/agent/tools/types";import type {  ComposerPromptTemplateRef,  ComposerSkillRef,} from "@/features/agent/composer-context"; import {  decodeRuntimeEventPayload,  decodeRuntimeSessions,  decodeRuntimeStatusResponse,  type RuntimeContextUsage,  type RuntimeEventPayload,  type RuntimeSessionSummary,  type RuntimeStatus,} from "@/features/agent/runtime/runtime-schema";export type { RuntimeContextUsage, RuntimeEventPayload, RuntimeSessionSummary, RuntimeStatus }; export function runtimeContextUsage(  status: RuntimeStatus | null | undefined,  fallback: RuntimeContextUsage | null | undefined,): RuntimeContextUsage | null {  if (status) return status.contextUsage ?? null;  return fallback ?? null;} const fetchEffect = (  input: RequestInfo | URL,  init?: RequestInit,): Effect.Effect<Response, unknown> =>  Effect.tryPromise({    try: () => fetch(input, init),    catch: (error) => error,  }); const safeJsonEffect = <T>(response: Response): Effect.Effect<T, unknown> =>  Effect.tryPromise({    try: () => safeJson<T>(response),    catch: (error) => error,  }); const AbortSessionResponseSchema = Schema.Struct({  ok: Schema.Boolean,  cleared: Schema.Struct({    steering: Schema.Array(Schema.String),    followUp: Schema.Array(Schema.String),  }),}); const decodeAbortSessionResponse = Schema.decodeUnknownOption(AbortSessionResponseSchema, {  onExcessProperty: "preserve",}); export type AbortSessionResult = {  steering: string[];  followUp: string[];}; export function parseAbortSessionResult(input: unknown): AbortSessionResult {  const decoded = decodeAbortSessionResponse(input);  return decoded._tag === "Some"    ? {        steering: [...decoded.value.cleared.steering],        followUp: [...decoded.value.cleared.followUp],      }    : { steering: [], followUp: [] };} export function listRuntimeSessions(): Promise<RuntimeSessionSummary[]> {  return Effect.runPromise(    Effect.gen(function* () {      const response = yield* fetchEffect("/api/agent/runtime/sessions", { cache: "no-store" });      const payload = yield* safeJsonEffect<unknown>(response);      return decodeRuntimeSessions(payload);    }).pipe(Effect.catch(() => Effect.succeed([]))),  );} export function loadRuntimeStatus(  sessionId: string,  piSessionId?: string | null,): Promise<RuntimeStatus | null> {  return Effect.runPromise(    Effect.gen(function* () {      const params = new URLSearchParams({ sessionId });      if (piSessionId) params.set("piSessionId", piSessionId);      const response = yield* fetchEffect(`/api/agent/runtime/status?${params.toString()}`, {        cache: "no-store",      });      const payload = yield* safeJsonEffect<unknown>(response);      const decoded = decodeRuntimeStatusResponse(payload);      if (!decoded) return null;      return { ...decoded.status, events: decoded.events ?? [] };    }).pipe(Effect.catch(() => Effect.succeed(null))),  );} export function abortSession(sessionId: string): Promise<AbortSessionResult> {  return Effect.runPromise(    Effect.gen(function* () {      const response = yield* fetchEffect("/api/agent/abort", {        method: "POST",        headers: { "Content-Type": "application/json" },        body: JSON.stringify({ sessionId }),      });      const payload = yield* safeJsonEffect<unknown>(response);      return parseAbortSessionResult(payload);    }).pipe(Effect.catch(() => Effect.succeed({ steering: [], followUp: [] }))),  );} export function respondExtensionUi(  sessionId: string,  requestId: string,  response: { value?: string; confirmed?: boolean; cancelled?: boolean },): Promise<void> {  return Effect.runPromise(    Effect.gen(function* () {      const result = yield* fetchEffect("/api/agent/runtime/extension-ui", {        method: "POST",        headers: { "Content-Type": "application/json" },        body: JSON.stringify({ sessionId, requestId, ...response }),      });      if (!result.ok) return yield* Effect.fail(new Error("Extension response was rejected"));    }),  );} /** What the session has spent over its whole life. Distinct from the context *  window, which compaction resets — this does not. */export type SessionUsageTotals = {  input: number;  output: number;  cacheRead: number;  cacheWrite: number;  reasoning: number;  total: number;  cost: number;  calls: number;  compactions: number;}; export type CanonicalSessionMeta = {  title: string | null;  modelId: string | null;  startedAt: string | null;  piSessionId: string | null;  usage?: SessionUsageTotals | null;}; export type CanonicalSessionResult = {  events: Record<string, unknown>[];  // 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<CanonicalSessionResult> {  return Effect.runPromise(    Effect.gen(function* () {      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));      const response = yield* fetchEffect(        `/api/agent/sessions/${encodeURIComponent(piSessionId)}?${params.toString()}`,        { cache: "no-store" },      );      const payload = yield* safeJsonEffect<{        events?: Record<string, unknown>[];        cursor?: number | null;        meta?: CanonicalSessionMeta | null;        error?: string;      }>(response);      if (!response.ok)        return yield* Effect.fail(new Error(payload.error || "Failed to load session"));      return {        events: payload.events ?? [],        cursor: payload.cursor ?? null,        meta: payload.meta ?? null,      };    }),  );} export type CompactSessionArgs = {  sessionId: string;  modelId: string;  thinkingLevel?: import("@/features/agent/contracts").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<CompactSessionResult> {  return Effect.runPromise(    Effect.gen(function* () {      const response = yield* fetchEffect("/api/agent/compact", {        method: "POST",        headers: { "Content-Type": "application/json" },        body: JSON.stringify(args),      });      const payload = yield* safeJsonEffect<{        error?: string;        status?: RuntimeStatus;      }>(response);      if (!response.ok) return yield* Effect.fail(new Error(payload.error || "Compaction failed"));      return payload;    }),  );} export type SubmitTurnArgs = {  sessionId: string;  modelId: string;  thinkingLevel?: import("@/features/agent/contracts").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<AgentTurnCommandResult> {  return Effect.runPromise(    Effect.gen(function* () {      const response = yield* fetchEffect("/api/agent/turn", {        method: "POST",        headers: { "Content-Type": "application/json" },        body: JSON.stringify(args),      });      const payload = yield* safeJsonEffect<{ error?: string } & Partial<AgentTurnCommandResult>>(        response,      );      const parsed = parseAgentTurnCommandResult(payload);      if (!response.ok || !parsed) {        return yield* Effect.fail(          new Error(payload.error || `Agent request failed: ${response.status}`),        );      }      if (parsed.outcome === "rejected") {        return yield* Effect.fail(new Error(parsed.error || "Agent request was rejected"));      }      return parsed;    }),  );} export type RuntimeEventSubscription = { close: () => void }; export function subscribeRuntimeEvents(  sessionId: string,  after: number,  piSessionId: string | null | undefined,  handlers: {    onPayload: (payload: RuntimeEventPayload) => void;    onError: () => void;  },): RuntimeEventSubscription {  const params = new URLSearchParams({ sessionId, after: String(after) });  if (piSessionId) params.set("piSessionId", piSessionId);  const source = new EventSource(`/api/agent/runtime/events?${params.toString()}`);  source.onmessage = (event) => {    let parsed: unknown;    try {      parsed = JSON.parse(event.data);    } catch {      return;    }    const payload = decodeRuntimeEventPayload(parsed);    if (!payload) return;    handlers.onPayload(payload);  };  source.onerror = handlers.onError;  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<SessionGoal | null> {  return Effect.runPromise(    Effect.gen(function* () {      const response = yield* fetchEffect(sessionGoalUrl(piSessionId), { cache: "no-store" });      if (!response.ok) return null;      const payload = yield* safeJsonEffect<unknown>(response);      return decodeSessionGoal(payload);    }).pipe(Effect.catch(() => Effect.succeed(null))),  );} export function updateSessionGoal(  piSessionId: string,  patch: SessionGoalPatch,): Promise<SessionGoal | null> {  return Effect.runPromise(    Effect.gen(function* () {      const response = yield* fetchEffect(sessionGoalUrl(piSessionId), {        method: "PUT",        headers: { "Content-Type": "application/json" },        body: JSON.stringify(patch),      });      if (!response.ok) return yield* Effect.fail(new Error("Failed to update the goal."));      const payload = yield* safeJsonEffect<unknown>(response);      return decodeSessionGoal(payload);    }),  );} export function clearSessionGoal(piSessionId: string): Promise<void> {  return Effect.runPromise(    Effect.gen(function* () {      const response = yield* fetchEffect(sessionGoalUrl(piSessionId), { method: "DELETE" });      if (!response.ok) return yield* Effect.fail(new Error("Failed to clear the goal."));    }),  );}