1729 lines · 52.7 KB · TypeScript
RawDownload
/** * CodexAdapterLive - Scoped live implementation for the Codex provider adapter. * * Wraps the typed Codex session runtime behind the `CodexAdapter` service * contract and maps runtime failures into the shared `ProviderAdapterError` * algebra. * * @module CodexAdapterLive */import {  type CanonicalItemType,  type CanonicalRequestType,  type CodexSettings,  ProviderDriverKind,  type ProviderEvent,  ProviderInstanceId,  type ProviderRuntimeEvent,  type ProviderRequestKind,  type ThreadTokenUsageSnapshot,  type ProviderUserInputAnswers,  RuntimeItemId,  RuntimeRequestId,  ProviderApprovalDecision,  ThreadId,  ProviderSendTurnInput,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as Crypto from "effect/Crypto";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as FileSystem from "effect/FileSystem";import * as Queue from "effect/Queue";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import { ChildProcessSpawner } from "effect/unstable/process";import * as CodexErrors from "effect-codex-app-server/errors";import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts";import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import {  ProviderAdapterRequestError,  ProviderAdapterProcessError,  ProviderAdapterSessionClosedError,  ProviderAdapterSessionNotFoundError,  ProviderAdapterValidationError,  type ProviderAdapterError,} from "../Errors.ts";import { type CodexAdapterShape } from "../Services/CodexAdapter.ts";import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import {  CodexResumeCursorSchema,  CodexSessionRuntimeThreadIdMissingError,  makeCodexSessionRuntime,  type CodexSessionRuntimeError,  type CodexSessionRuntimeOptions,  type CodexSessionRuntimeShape,} from "./CodexSessionRuntime.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";import { resolveCodexLaunchArgs } from "./codexLaunchArgs.ts";const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError);const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError);const isCodexSessionRuntimeThreadIdMissingError = Schema.is(  CodexSessionRuntimeThreadIdMissingError,);const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema); const PROVIDER = ProviderDriverKind.make("codex"); export interface CodexAdapterLiveOptions {  readonly instanceId?: ProviderInstanceId;  readonly environment?: NodeJS.ProcessEnv;  readonly makeRuntime?: (    options: CodexSessionRuntimeOptions,  ) => Effect.Effect<    CodexSessionRuntimeShape,    CodexSessionRuntimeError,    ChildProcessSpawner.ChildProcessSpawner | Scope.Scope  >;  readonly nativeEventLogPath?: string;  readonly nativeEventLogger?: EventNdjsonLogger;} interface CodexAdapterSessionContext {  readonly threadId: ThreadId;  readonly scope: Scope.Closeable;  readonly runtime: CodexSessionRuntimeShape;  readonly eventFiber: Fiber.Fiber<void, never>;  stopped: boolean;} function mapCodexRuntimeError(  threadId: ThreadId,  method: string,  error: CodexSessionRuntimeError,): ProviderAdapterError {  if (isCodexAppServerProcessExitedError(error) || isCodexAppServerTransportError(error)) {    return new ProviderAdapterSessionClosedError({      provider: PROVIDER,      threadId,      cause: error,    });  }   if (isCodexSessionRuntimeThreadIdMissingError(error)) {    return new ProviderAdapterSessionNotFoundError({      provider: PROVIDER,      threadId,      cause: error,    });  }   return new ProviderAdapterRequestError({    provider: PROVIDER,    method,    detail: error.message,    cause: error,  });} type CodexLifecycleItem =  | EffectCodexSchema.V2ItemStartedNotification["item"]  | EffectCodexSchema.V2ItemCompletedNotification["item"]; type CodexToolUserInputQuestion =  | EffectCodexSchema.ServerRequest__ToolRequestUserInputQuestion  | EffectCodexSchema.ToolRequestUserInputParams__ToolRequestUserInputQuestion; const ApprovalDecisionPayload = Schema.Struct({  decision: ProviderApprovalDecision,}); function readPayload<A>(  schema: Schema.Schema<A>,  payload: ProviderEvent["payload"],): A | undefined {  const isPayload = Schema.is(schema);  return isPayload(payload) ? payload : undefined;} function trimText(value: string | undefined | null): string | undefined {  const trimmed = value?.trim();  return trimmed && trimmed.length > 0 ? trimmed : undefined;} const FATAL_CODEX_STDERR_SNIPPETS = ["failed to connect to websocket"]; function isFatalCodexProcessStderrMessage(message: string): boolean {  const normalized = message.toLowerCase();  return FATAL_CODEX_STDERR_SNIPPETS.some((snippet) => normalized.includes(snippet));} function normalizeCodexTokenUsage(  usage: EffectCodexSchema.V2ThreadTokenUsageUpdatedNotification["tokenUsage"],): ThreadTokenUsageSnapshot | undefined {  const totalProcessedTokens = usage.total.totalTokens;  const usedTokens = usage.last.totalTokens;  if (usedTokens === undefined || usedTokens <= 0) {    return undefined;  }   const maxTokens = usage.modelContextWindow ?? undefined;  const inputTokens = usage.last.inputTokens;  const cachedInputTokens = usage.last.cachedInputTokens;  const outputTokens = usage.last.outputTokens;  const reasoningOutputTokens = usage.last.reasoningOutputTokens;   return {    usedTokens,    ...(totalProcessedTokens !== undefined && totalProcessedTokens > usedTokens      ? { totalProcessedTokens }      : {}),    ...(maxTokens !== undefined ? { maxTokens } : {}),    ...(inputTokens !== undefined ? { inputTokens } : {}),    ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),    ...(outputTokens !== undefined ? { outputTokens } : {}),    ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),    ...(usedTokens !== undefined ? { lastUsedTokens: usedTokens } : {}),    ...(inputTokens !== undefined ? { lastInputTokens: inputTokens } : {}),    ...(cachedInputTokens !== undefined ? { lastCachedInputTokens: cachedInputTokens } : {}),    ...(outputTokens !== undefined ? { lastOutputTokens: outputTokens } : {}),    ...(reasoningOutputTokens !== undefined      ? { lastReasoningOutputTokens: reasoningOutputTokens }      : {}),    compactsAutomatically: true,  };} function toTurnStatus(  value: EffectCodexSchema.V2TurnCompletedNotification["turn"]["status"] | "cancelled",): "completed" | "failed" | "cancelled" | "interrupted" {  switch (value) {    case "completed":    case "failed":    case "cancelled":    case "interrupted":      return value;    default:      return "completed";  }} function normalizeItemType(raw: string | undefined | null): string {  const type = trimText(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: string | undefined | null): 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, item?: CodexLifecycleItem): string | undefined {  if (itemType === "mcp_tool_call" && item?.type === "mcpToolCall") {    return `${item.server} · ${item.tool}`;  }  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 "Ran command";    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(itemType: CanonicalItemType, item: CodexLifecycleItem): string | undefined {  const itemRecord = item as Record<string, unknown>;  const action = itemRecord.action as Record<string, unknown> | undefined;  const actionQueries = Array.isArray(action?.queries) ? action.queries : [];  const candidates = [    ...(itemType === "web_search"      ? [itemRecord.query, action?.query, ...actionQueries, action?.pattern, action?.url]      : []),    "command" in item ? item.command : undefined,    "title" in item ? item.title : undefined,    "summary" in item ? item.summary : undefined,    "text" in item ? item.text : undefined,    "path" in item ? item.path : undefined,    "prompt" in item ? item.prompt : undefined,  ];   for (const candidate of candidates) {    const trimmed = typeof candidate === "string" ? trimText(candidate) : undefined;    if (!trimmed) 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: ProviderRequestKind | undefined): 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 toCanonicalUserInputAnswers(  answers: EffectCodexSchema.ToolRequestUserInputResponse["answers"],): ProviderUserInputAnswers {  return Object.fromEntries(    Object.entries(answers).map(([questionId, value]) => {      const normalizedAnswers = value.answers.length === 1 ? value.answers[0]! : [...value.answers];      return [questionId, normalizedAnswers] as const;    }),  );} function toUserInputQuestions(questions: ReadonlyArray<CodexToolUserInputQuestion>) {  const parsedQuestions = questions    .map((question) => {      const options =        question.options          ?.map((option) => {            const label = trimText(option.label);            const description = trimText(option.description);            if (!label || !description) {              return undefined;            }            return { label, description };          })          .filter((option) => option !== undefined) ?? [];       const id = trimText(question.id);      const header = trimText(question.header);      const prompt = trimText(question.question);      if (!id || !header || !prompt || options.length === 0) {        return undefined;      }      return {        id,        header,        question: prompt,        options,        multiSelect: false,      };    })    .filter((question) => question !== undefined);   return parsedQuestions.length > 0 ? parsedQuestions : undefined;} function toThreadState(  status: EffectCodexSchema.V2ThreadStatusChangedNotification["status"],): "active" | "idle" | "archived" | "closed" | "compacted" | "error" {  switch (status.type) {    case "idle":      return "idle";    case "systemError":      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";  }} function asRuntimeItemId(itemId: ProviderEvent["itemId"] & string): RuntimeItemId {  return RuntimeItemId.make(itemId);} function asRuntimeRequestId(requestId: string): RuntimeRequestId {  return RuntimeRequestId.make(requestId);} 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 =    readPayload(EffectCodexSchema.V2ItemStartedNotification, event.payload) ??    readPayload(EffectCodexSchema.V2ItemCompletedNotification, event.payload);  const item = payload?.item;  if (!item) {    return undefined;  }  const itemType = toCanonicalItemType(item.type);  if (itemType === "unknown" && lifecycle !== "item.updated") {    return undefined;  }   const detail = itemDetail(itemType, item);  const status =    lifecycle === "item.started"      ? "inProgress"      : lifecycle === "item.completed"        ? "completed"        : undefined;   return {    ...runtimeEventBase(event, canonicalThreadId),    type: lifecycle,    payload: {      itemType,      ...(status ? { status } : {}),      ...(itemTitle(itemType, item) ? { title: itemTitle(itemType, item) } : {}),      ...(detail ? { detail } : {}),      ...(event.payload !== undefined ? { data: event.payload } : {}),    },  };} function mapToRuntimeEvents(  event: ProviderEvent,  canonicalThreadId: ThreadId,): ReadonlyArray<ProviderRuntimeEvent> {

Lines 1–500 of 1729.