1517 lines · 44.7 KB · TypeScript
RawDownload
/** * CodexAdapterLive - Scoped live implementation for the Codex provider adapter. * * Wraps `CodexAppServerManager` behind the `CodexAdapter` service contract and * maps manager failures into the shared `ProviderAdapterError` algebra. * * @module CodexAdapterLive */import {  type CanonicalItemType,  type CanonicalRequestType,  type ProviderEvent,  type ProviderRuntimeEvent,  type ProviderUserInputAnswers,  RuntimeItemId,  RuntimeRequestId,  RuntimeTaskId,  ProviderApprovalDecision,  ProviderItemId,  ThreadId,  TurnId,} from "@t3tools/contracts";import { Effect, FileSystem, Layer, Queue, Schema, ServiceMap, Stream } from "effect"; import {  ProviderAdapterProcessError,  ProviderAdapterRequestError,  ProviderAdapterSessionClosedError,  ProviderAdapterSessionNotFoundError,  ProviderAdapterValidationError,  type ProviderAdapterError,} from "../Errors.ts";import { CodexAdapter, type CodexAdapterShape } from "../Services/CodexAdapter.ts";import {  CodexAppServerManager,  type CodexAppServerStartSessionInput,} from "../../codexAppServerManager.ts";import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const PROVIDER = "codex" as const; export interface CodexAdapterLiveOptions {  readonly manager?: CodexAppServerManager;  readonly makeManager?: (services?: ServiceMap.ServiceMap<never>) => CodexAppServerManager;  readonly nativeEventLogPath?: string;  readonly nativeEventLogger?: EventNdjsonLogger;} function toMessage(cause: unknown, fallback: string): string {  if (cause instanceof Error && cause.message.length > 0) {    return cause.message;  }  return fallback;} function toSessionError(  threadId: ThreadId,  cause: unknown,): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {  const normalized = toMessage(cause, "").toLowerCase();  if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) {    return new ProviderAdapterSessionNotFoundError({      provider: PROVIDER,      threadId,      cause,    });  }  if (normalized.includes("session is closed")) {    return new ProviderAdapterSessionClosedError({      provider: PROVIDER,      threadId,      cause,    });  }  return undefined;} function toRequestError(threadId: ThreadId, method: string, cause: unknown): ProviderAdapterError {  const sessionError = toSessionError(threadId, cause);  if (sessionError) {    return sessionError;  }  return new ProviderAdapterRequestError({    provider: PROVIDER,    method,    detail: toMessage(cause, `${method} failed`),    cause,  });} function asObject(value: unknown): Record<string, unknown> | undefined {  if (!value || typeof value !== "object") {    return undefined;  }  return value as Record<string, unknown>;} function asString(value: unknown): string | undefined {  return typeof value === "string" ? value : undefined;} function asArray(value: unknown): unknown[] | undefined {  return Array.isArray(value) ? value : undefined;} function asNumber(value: unknown): number | undefined {  return typeof value === "number" && Number.isFinite(value) ? value : undefined;} function toTurnStatus(value: unknown): "completed" | "failed" | "cancelled" | "interrupted" {  switch (value) {    case "completed":    case "failed":    case "cancelled":    case "interrupted":      return value;    default:      return "completed";  }} function normalizeItemType(raw: unknown): string {  const type = asString(raw);  if (!type) return "item";  return type    .replace(/([a-z0-9])([A-Z])/g, "$1 $2")    .replace(/[._/-]/g, " ")    .replace(/\s+/g, " ")    .trim()    .toLowerCase();} function toCanonicalItemType(raw: unknown): CanonicalItemType {  const type = normalizeItemType(raw);  if (type.includes("user")) return "user_message";  if (type.includes("agent message") || type.includes("assistant")) return "assistant_message";  if (type.includes("reasoning") || type.includes("thought")) return "reasoning";  if (type.includes("plan") || type.includes("todo")) return "plan";  if (type.includes("command")) return "command_execution";  if (type.includes("file change") || type.includes("patch") || type.includes("edit"))    return "file_change";  if (type.includes("mcp")) return "mcp_tool_call";  if (type.includes("dynamic tool")) return "dynamic_tool_call";  if (type.includes("collab")) return "collab_agent_tool_call";  if (type.includes("web search")) return "web_search";  if (type.includes("image")) return "image_view";  if (type.includes("review entered")) return "review_entered";  if (type.includes("review exited")) return "review_exited";  if (type.includes("compact")) return "context_compaction";  if (type.includes("error")) return "error";  return "unknown";} function itemTitle(itemType: CanonicalItemType): string | undefined {  switch (itemType) {    case "assistant_message":      return "Assistant message";    case "user_message":      return "User message";    case "reasoning":      return "Reasoning";    case "plan":      return "Plan";    case "command_execution":      return "Command run";    case "file_change":      return "File change";    case "mcp_tool_call":      return "MCP tool call";    case "dynamic_tool_call":      return "Tool call";    case "web_search":      return "Web search";    case "image_view":      return "Image view";    case "error":      return "Error";    default:      return undefined;  }} function itemDetail(  item: Record<string, unknown>,  payload: Record<string, unknown>,): string | undefined {  const nestedResult = asObject(item.result);  const candidates = [    asString(item.command),    asString(item.title),    asString(item.summary),    asString(item.text),    asString(item.path),    asString(item.prompt),    asString(nestedResult?.command),    asString(payload.command),    asString(payload.message),    asString(payload.prompt),  ];  for (const candidate of candidates) {    if (!candidate) continue;    const trimmed = candidate.trim();    if (trimmed.length === 0) continue;    return trimmed;  }  return undefined;} function toRequestTypeFromMethod(method: string): CanonicalRequestType {  switch (method) {    case "item/commandExecution/requestApproval":      return "command_execution_approval";    case "item/fileRead/requestApproval":      return "file_read_approval";    case "item/fileChange/requestApproval":      return "file_change_approval";    case "applyPatchApproval":      return "apply_patch_approval";    case "execCommandApproval":      return "exec_command_approval";    case "item/tool/requestUserInput":      return "tool_user_input";    case "item/tool/call":      return "dynamic_tool_call";    case "account/chatgptAuthTokens/refresh":      return "auth_tokens_refresh";    default:      return "unknown";  }} function toRequestTypeFromKind(kind: unknown): CanonicalRequestType {  switch (kind) {    case "command":      return "command_execution_approval";    case "file-read":      return "file_read_approval";    case "file-change":      return "file_change_approval";    default:      return "unknown";  }} function toRequestTypeFromResolvedPayload(  payload: Record<string, unknown> | undefined,): CanonicalRequestType {  const request = asObject(payload?.request);  const method = asString(request?.method) ?? asString(payload?.method);  if (method) {    return toRequestTypeFromMethod(method);  }  const requestKind = asString(request?.kind) ?? asString(payload?.requestKind);  if (requestKind) {    return toRequestTypeFromKind(requestKind);  }  return "unknown";} function toCanonicalUserInputAnswers(  answers: ProviderUserInputAnswers | undefined,): ProviderUserInputAnswers {  if (!answers) {    return {};  }   return Object.fromEntries(    Object.entries(answers).flatMap(([questionId, value]) => {      if (typeof value === "string") {        return [[questionId, value] as const];      }       if (Array.isArray(value)) {        const normalized = value.filter((entry): entry is string => typeof entry === "string");        return [[questionId, normalized.length === 1 ? normalized[0] : normalized] as const];      }       const answerObject = asObject(value);      const answerList = asArray(answerObject?.answers)?.filter(        (entry): entry is string => typeof entry === "string",      );      if (!answerList) {        return [];      }      return [[questionId, answerList.length === 1 ? answerList[0] : answerList] as const];    }),  );} function toUserInputQuestions(payload: Record<string, unknown> | undefined) {  const questions = asArray(payload?.questions);  if (!questions) {    return undefined;  }   const parsedQuestions = questions    .map((entry) => {      const question = asObject(entry);      if (!question) return undefined;      const options = asArray(question.options)        ?.map((option) => {          const optionRecord = asObject(option);          if (!optionRecord) return undefined;          const label = asString(optionRecord.label)?.trim();          const description = asString(optionRecord.description)?.trim();          if (!label || !description) {            return undefined;          }          return { label, description };        })        .filter((option): option is { label: string; description: string } => option !== undefined);      const id = asString(question.id)?.trim();      const header = asString(question.header)?.trim();      const prompt = asString(question.question)?.trim();      if (!id || !header || !prompt || !options || options.length === 0) {        return undefined;      }      return {        id,        header,        question: prompt,        options,      };    })    .filter(      (        question,      ): question is {        id: string;        header: string;        question: string;        options: Array<{ label: string; description: string }>;      } => question !== undefined,    );   return parsedQuestions.length > 0 ? parsedQuestions : undefined;} function toThreadState(  value: unknown,): "active" | "idle" | "archived" | "closed" | "compacted" | "error" {  switch (value) {    case "idle":      return "idle";    case "archived":      return "archived";    case "closed":      return "closed";    case "compacted":      return "compacted";    case "error":    case "failed":      return "error";    default:      return "active";  }} function contentStreamKindFromMethod(  method: string,):  | "assistant_text"  | "reasoning_text"  | "reasoning_summary_text"  | "plan_text"  | "command_output"  | "file_change_output" {  switch (method) {    case "item/agentMessage/delta":      return "assistant_text";    case "item/reasoning/textDelta":      return "reasoning_text";    case "item/reasoning/summaryTextDelta":      return "reasoning_summary_text";    case "item/commandExecution/outputDelta":      return "command_output";    case "item/fileChange/outputDelta":      return "file_change_output";    default:      return "assistant_text";  }} const PROPOSED_PLAN_BLOCK_REGEX = /<proposed_plan>\s*([\s\S]*?)\s*<\/proposed_plan>/i; function extractProposedPlanMarkdown(text: string | undefined): string | undefined {  const match = text ? PROPOSED_PLAN_BLOCK_REGEX.exec(text) : null;  const planMarkdown = match?.[1]?.trim();  return planMarkdown && planMarkdown.length > 0 ? planMarkdown : undefined;} function asRuntimeItemId(itemId: ProviderItemId): RuntimeItemId {  return RuntimeItemId.makeUnsafe(itemId);} function asRuntimeRequestId(requestId: string): RuntimeRequestId {  return RuntimeRequestId.makeUnsafe(requestId);} function asRuntimeTaskId(taskId: string): RuntimeTaskId {  return RuntimeTaskId.makeUnsafe(taskId);} function codexEventMessage(payload: Record<string, unknown> | undefined): Record<string, unknown> | undefined {  return asObject(payload?.msg);} function codexEventBase(  event: ProviderEvent,  canonicalThreadId: ThreadId,): Omit<ProviderRuntimeEvent, "type" | "payload"> {  const payload = asObject(event.payload);  const msg = codexEventMessage(payload);  const turnId = asString(msg?.turn_id) ?? asString(msg?.turnId);  const itemId = asString(msg?.item_id) ?? asString(msg?.itemId);  const requestId = asString(msg?.request_id) ?? asString(msg?.requestId);  const base = runtimeEventBase(event, canonicalThreadId);  const providerRefs = base.providerRefs    ? {        ...base.providerRefs,        ...(turnId ? { providerTurnId: turnId } : {}),        ...(itemId ? { providerItemId: ProviderItemId.makeUnsafe(itemId) } : {}),        ...(requestId ? { providerRequestId: requestId } : {}),      }    : {        ...(turnId ? { providerTurnId: turnId } : {}),        ...(itemId ? { providerItemId: ProviderItemId.makeUnsafe(itemId) } : {}),        ...(requestId ? { providerRequestId: requestId } : {}),      };   return {    ...base,    ...(turnId ? { turnId: TurnId.makeUnsafe(turnId) } : {}),    ...(itemId ? { itemId: asRuntimeItemId(ProviderItemId.makeUnsafe(itemId)) } : {}),    ...(requestId ? { requestId: asRuntimeRequestId(requestId) } : {}),    ...(Object.keys(providerRefs).length > 0 ? { providerRefs } : {}),  };} function eventRawSource(event: ProviderEvent): NonNullable<ProviderRuntimeEvent["raw"]>["source"] {  return event.kind === "request" ? "codex.app-server.request" : "codex.app-server.notification";} function providerRefsFromEvent(  event: ProviderEvent,): ProviderRuntimeEvent["providerRefs"] | undefined {  const refs: Record<string, string> = {};  if (event.turnId) refs.providerTurnId = event.turnId;  if (event.itemId) refs.providerItemId = event.itemId;  if (event.requestId) refs.providerRequestId = event.requestId;   return Object.keys(refs).length > 0 ? (refs as ProviderRuntimeEvent["providerRefs"]) : undefined;} function runtimeEventBase(  event: ProviderEvent,  canonicalThreadId: ThreadId,): Omit<ProviderRuntimeEvent, "type" | "payload"> {  const refs = providerRefsFromEvent(event);  return {    eventId: event.id,    provider: event.provider,    threadId: canonicalThreadId,    createdAt: event.createdAt,    ...(event.turnId ? { turnId: event.turnId } : {}),    ...(event.itemId ? { itemId: asRuntimeItemId(event.itemId) } : {}),    ...(event.requestId ? { requestId: asRuntimeRequestId(event.requestId) } : {}),    ...(refs ? { providerRefs: refs } : {}),    raw: {      source: eventRawSource(event),      method: event.method,      payload: event.payload ?? {},    },  };} function mapItemLifecycle(  event: ProviderEvent,  canonicalThreadId: ThreadId,  lifecycle: "item.started" | "item.updated" | "item.completed",): ProviderRuntimeEvent | undefined {  const payload = asObject(event.payload);  const item = asObject(payload?.item);  const source = item ?? payload;  if (!source) {    return undefined;  }   const itemType = toCanonicalItemType(source.type ?? source.kind);  if (itemType === "unknown" && lifecycle !== "item.updated") {    return undefined;  }   const detail = itemDetail(source, payload ?? {});  const status =    lifecycle === "item.started"      ? "inProgress"      : lifecycle === "item.completed"

Lines 1–500 of 1517.