1531 lines · 46.8 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 CanonicalToolLifecycleData,  type CanonicalRequestType,  type ProviderEvent,  type ProviderRuntimeEvent,  type ThreadTokenUsageSnapshot,  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 normalizeCommandValue(value: unknown): string | undefined {  if (typeof value === "string") {    const trimmed = value.trim();    return trimmed.length > 0 ? trimmed : undefined;  }  if (!Array.isArray(value)) {    return undefined;  }  const parts = value    .map((entry) => (typeof entry === "string" ? entry.trim() : ""))    .filter((entry) => entry.length > 0);  return parts.length > 0 ? parts.join(" ") : undefined;} function pushChangedFile(target: string[], seen: Set<string>, value: unknown) {  if (typeof value !== "string") {    return;  }  const normalized = value.trim();  if (normalized.length === 0 || seen.has(normalized)) {    return;  }  seen.add(normalized);  target.push(normalized);} function collectChangedFiles(value: unknown, target: string[], seen: Set<string>, depth: number) {  if (depth > 4 || target.length >= 12) {    return;  }  if (Array.isArray(value)) {    for (const entry of value) {      collectChangedFiles(entry, target, seen, depth + 1);      if (target.length >= 12) {        return;      }    }    return;  }   const record = asObject(value);  if (!record) {    return;  }   pushChangedFile(target, seen, record.path);  pushChangedFile(target, seen, record.filePath);  pushChangedFile(target, seen, record.relativePath);  pushChangedFile(target, seen, record.filename);  pushChangedFile(target, seen, record.newPath);  pushChangedFile(target, seen, record.oldPath);   for (const nestedKey of ["item", "result", "input", "changes", "files", "edits", "patches"]) {    if (!(nestedKey in record)) {      continue;    }    collectChangedFiles(record[nestedKey], target, seen, depth + 1);  }} function buildCanonicalToolLifecycleData(  itemType: CanonicalItemType,  source: Record<string, unknown>,  payload: Record<string, unknown>,): CanonicalToolLifecycleData | undefined {  const result = asObject(source.result);  const input = asObject(source.input);  const command = [    normalizeCommandValue(source.command),    normalizeCommandValue(input?.command),    normalizeCommandValue(result?.command),    normalizeCommandValue(payload.command),  ].find((candidate) => candidate !== undefined);  const changedFiles: string[] = [];  const seen = new Set<string>();  collectChangedFiles(source, changedFiles, seen, 0);  collectChangedFiles(payload, changedFiles, seen, 0);  if (itemType === "command_execution" && command) {    return {      kind: "command_execution",      command,      ...(input ? { input: input as CanonicalToolLifecycleData["input"] } : {}),      ...(result ? { result: result as CanonicalToolLifecycleData["result"] } : {}),      ...(typeof result?.content === "string" ? { output: result.content } : {}),      ...(asNumber(result?.exitCode) !== undefined ? { exitCode: asNumber(result?.exitCode) } : {}),    };  }  if (itemType === "file_change" && changedFiles.length > 0) {    return {      kind: "file_change",      changedFiles,      ...(input ? { input: input as CanonicalToolLifecycleData["input"] } : {}),      ...(result ? { result: result as CanonicalToolLifecycleData["result"] } : {}),    };  }  if (input || result) {    return {      kind: "generic",      ...(input ? { input: input as CanonicalToolLifecycleData["input"] } : {}),      ...(result ? { result: result as CanonicalToolLifecycleData["result"] } : {}),    };  }  return undefined;} function normalizeCodexTokenUsage(value: unknown): ThreadTokenUsageSnapshot | undefined {  const usage = asObject(value);  const totalUsage = asObject(usage?.total_token_usage ?? usage?.total);  const lastUsage = asObject(usage?.last_token_usage ?? usage?.last);   const totalProcessedTokens =    asNumber(totalUsage?.total_tokens) ?? asNumber(totalUsage?.totalTokens);  const usedTokens =    asNumber(lastUsage?.total_tokens) ?? asNumber(lastUsage?.totalTokens) ?? totalProcessedTokens;  if (usedTokens === undefined || usedTokens <= 0) {    return undefined;  }   const maxTokens = asNumber(usage?.model_context_window) ?? asNumber(usage?.modelContextWindow);  const inputTokens = asNumber(lastUsage?.input_tokens) ?? asNumber(lastUsage?.inputTokens);  const cachedInputTokens =    asNumber(lastUsage?.cached_input_tokens) ?? asNumber(lastUsage?.cachedInputTokens);  const outputTokens = asNumber(lastUsage?.output_tokens) ?? asNumber(lastUsage?.outputTokens);  const reasoningOutputTokens =    asNumber(lastUsage?.reasoning_output_tokens) ?? asNumber(lastUsage?.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 toTurnId(value: string | undefined): TurnId | undefined {  return value?.trim() ? TurnId.makeUnsafe(value) : undefined;} function toProviderItemId(value: string | undefined): ProviderItemId | undefined {  return value?.trim() ? ProviderItemId.makeUnsafe(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 "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(  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 {};  }   const normalized: Record<string, string | readonly string[]> = {};  for (const [questionId, value] of Object.entries(answers)) {    if (typeof value === "string") {      normalized[questionId] = value;      continue;    }     if (Array.isArray(value)) {      const entries = value.filter((entry): entry is string => typeof entry === "string");      normalized[questionId] = entries.length === 1 ? (entries[0] ?? entries) : entries;      continue;    }     const answerObject = asObject(value);    const answerList = asArray(answerObject?.answers)?.filter(      (entry): entry is string => typeof entry === "string",    );    if (answerList) {      normalized[questionId] = answerList.length === 1 ? (answerList[0] ?? answerList) : answerList;    }  }   return normalized;} 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(

Lines 1–500 of 1531.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t