1import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
2import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { type ProviderReplayTranscript } from "@t3tools/contracts";
5import * as CodexClient from "effect-codex-app-server/client";
6import type * as CodexError from "effect-codex-app-server/errors";
7import * as CodexReplay from "effect-codex-app-server/replay";
8import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";
10import * as Layer from "effect/Layer";
11import * as Path from "effect/Path";
12import * as PlatformError from "effect/PlatformError";
13import * as Predicate from "effect/Predicate";
14import * as Schema from "effect/Schema";
15
16import * as ServerConfig from "../../config.ts";
17import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
18import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
19import { ProviderAdapterDriverCreateError } from "@t3tools/provider-core/server/adapterDriver";
20import * as ProviderAdapterRegistry from "../ProviderAdapterRegistry.ts";
21import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts";
22import type { ProviderReplayGate } from "@t3tools/provider-testing/replayGate";
23import * as CodexAdapterV2 from "./CodexAdapterV2.ts";
24
25export class CodexReplayTranscriptDecodeError extends Schema.TaggedError<CodexReplayTranscriptDecodeError>()(
26 "CodexReplayTranscriptDecodeError",
27 {
28 driver: Schema.optional(Schema.String),
29 protocol: Schema.optional(Schema.String),
30 scenario: Schema.optional(Schema.String),
31 cause: Schema.Defect(),
32 },
33) {
34 override get message(): string {
35 return `Failed to decode Codex app-server replay transcript for scenario ${this.scenario ?? "<unknown>"}.`;
36 }
37}
38
39export const CodexOrchestratorReplayHarnessError = Schema.Union([
40 CodexReplayTranscriptDecodeError,
41 CodexReplay.CodexAppServerReplayError,
42 ProviderAdapterDriverCreateError,
43]);
44export type CodexOrchestratorReplayHarnessError = typeof CodexOrchestratorReplayHarnessError.Type;
45
46export function withCodexReplayChildMetadata(
47 client: CodexClient.CodexAppServerClient["Service"],
48 transcript: CodexReplay.CodexAppServerReplayTranscript,
49 readMetadata: (
50 threadId: string,
51 method: "thread/read" | "thread/resume",
52 ) => Effect.Effect<unknown, CodexError.CodexAppServerError> = (threadId) =>
53 Effect.succeed({ thread: { id: threadId }, model: null }),
54): CodexClient.CodexAppServerClient["Service"] {
55 const childThreadIds = new Set(
56 transcript.entries.flatMap((entry) => {
57 if (entry.type !== "emit_inbound" || !Predicate.isObject(entry.frame)) return [];
58 const params = entry.frame.params;
59 if (!Predicate.isObject(params) || !Predicate.isObject(params.item)) return [];
60 const item = params.item;
61 if (item.type === "subAgentActivity" && typeof item.agentThreadId === "string") {
62 return [item.agentThreadId];
63 }
64 return item.type === "collabAgentToolCall" && Array.isArray(item.receiverThreadIds)
65 ? item.receiverThreadIds.filter(Predicate.isString)
66 : [];
67 }),
68 );
69 return {
70 ...client,
71 raw: {
72 ...client.raw,
73 request: (method, params) =>
74 (method === "thread/read" || method === "thread/resume") &&
75 Predicate.isObject(params) &&
76 (method === "thread/read" ? params.includeTurns === false : params.excludeTurns === true) &&
77 typeof params.threadId === "string" &&
78 childThreadIds.has(params.threadId)
79 ? readMetadata(params.threadId, method)
80 : client.raw.request(method, params),
81 },
82 };
83}
84
85function metadataFromTranscript(transcript: ProviderReplayTranscript): {
86 readonly provider?: string;
87 readonly protocol?: string;
88 readonly scenario?: string;
89} {
90 return {
91 provider: transcript.provider,
92 protocol: transcript.protocol,
93 scenario: transcript.scenario,
94 };
95}
96
97export function makeReplayServerConfig(
98 scenario: string,
99): Effect.Effect<
100 ServerConfig.ServerConfig["Service"],
101 PlatformError.PlatformError,
102 FileSystem.FileSystem | Path.Path
103> {
104 return Effect.gen(function* () {
105 const fs = yield* FileSystem.FileSystem;
106 const path = yield* Path.Path;
107 const baseDir = yield* fs.makeTempDirectory({
108 prefix: `t3-orchestration-v2-codex-${scenario}-`,
109 });
110 const stateDir = path.join(baseDir, "userdata");
111 const logsDir = path.join(stateDir, "logs");
112 const providerLogsDir = path.join(logsDir, "provider");
113 const terminalLogsDir = path.join(logsDir, "terminals");
114 const attachmentsDir = path.join(stateDir, "attachments");
115 const environmentThemesDir = path.join(stateDir, "themes");
116 const worktreesDir = path.join(baseDir, "worktrees");
117 const providerStatusCacheDir = path.join(baseDir, "caches");
118
119 for (const directory of [
120 stateDir,
121 logsDir,
122 providerLogsDir,
123 terminalLogsDir,
124 attachmentsDir,
125 environmentThemesDir,
126 worktreesDir,
127 providerStatusCacheDir,
128 ]) {
129 yield* fs.makeDirectory(directory, { recursive: true });
130 }
131
132 return {
133 logLevel: "Error",
134 traceMinLevel: "Info",
135 traceTimingEnabled: true,
136 traceBatchWindowMs: 200,
137 traceMaxBytes: 10 * 1024 * 1024,
138 traceMaxFiles: 10,
139 otelEnvironment: OtelEnvironment.none,
140 otlpTracesUrl: undefined,
141 otlpMetricsUrl: undefined,
142 otlpLogsUrl: undefined,
143 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
144 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
145 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
146 mode: "web",
147 port: 0,
148 host: undefined,
149 cwd: process.cwd(),
150 baseDir,
151 staticDir: undefined,
152 devUrl: undefined,
153 devAllowedOrigins: [],
154 noBrowser: false,
155 startupPresentation: "browser",
156 tailscaleServeEnabled: false,
157 tailscaleServePort: 443,
158 desktopBootstrapToken: undefined,
159 autoBootstrapProjectFromCwd: false,
160 logWebSocketEvents: false,
161 stateDir,
162 dbPath: path.join(stateDir, "state.sqlite"),
163 keybindingsConfigPath: path.join(stateDir, "keybindings.json"),
164 settingsPath: path.join(stateDir, "settings.json"),
165 providerStatusCacheDir,
166 worktreesDir,
167 attachmentsDir,
168 browserArtifactsDir: path.join(stateDir, "browser-artifacts"),
169 environmentThemesDir,
170 logsDir,
171 serverLogPath: path.join(logsDir, "server.log"),
172 serverTracePath: path.join(logsDir, "server.trace.ndjson"),
173 providerLogsDir,
174 providerEventLogPath: path.join(providerLogsDir, "events.log"),
175 terminalLogsDir,
176 environmentIdPath: path.join(stateDir, "environment-id"),
177 serverRuntimeStatePath: path.join(stateDir, "server-runtime.json"),
178 secretsDir: path.join(stateDir, "secrets"),
179 };
180 });
181}
182
183export function layer(input: {
184 readonly transcript: CodexReplay.CodexAppServerReplayTranscript;
185 readonly driver?: CodexReplay.CodexAppServerReplayDriver;
186}) {
187 const layerReplay =
188 input.driver === undefined
189 ? CodexReplay.layerReplay(input.transcript)
190 : CodexReplay.layerReplayWithDriver(input.driver);
191 const layerReplayClientFactory = Layer.succeed(CodexAdapterV2.CodexAppServerClientFactory, {
192 open: (openInput) =>
193 Effect.gen(function* () {
194 const context = yield* Layer.build(layerReplay).pipe(
195 Effect.mapError(
196 (cause) =>
197 new ProviderAdapter.ProviderAdapterOpenSessionError({
198 driver: CodexAdapterV2.CODEX_DRIVER_KIND,
199 providerSessionId: openInput.providerSessionId,
200 cause,
201 }),
202 ),
203 );
204 return yield* Effect.service(CodexClient.CodexAppServerClient).pipe(
205 Effect.map((client) => withCodexReplayChildMetadata(client, input.transcript)),
206 Effect.provide(context),
207 );
208 }),
209 });
210 const layerServerConfig = Layer.effect(
211 ServerConfig.ServerConfig,
212 makeReplayServerConfig(input.transcript.scenario).pipe(Effect.orDie),
213 ).pipe(Layer.provide(NodeServices.layer));
214 const layerRegistry = ProviderAdapterRegistry.layerFromDrivers({
215 drivers: [CodexAdapterV2.CodexAdapterV2Driver],
216 configMap: {
217 [CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID]: {
218 driver: CodexAdapterV2.CODEX_DRIVER_KIND,
219 },
220 },
221 }).pipe(
222 Layer.provide(
223 Layer.mergeAll(
224 layerReplayClientFactory,
225 layerServerConfig,
226 NodeServices.layer,
227 IdAllocator.layer,
228 ),
229 ),
230 );
231
232 return layerRegistry;
233}
234
235const decodeCodexAppServerReplayTranscript = Schema.decodeUnknownEffect(
236 CodexReplay.CodexAppServerReplayTranscript,
237);
238
239export const CodexOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<
240 CodexReplay.CodexAppServerReplayTranscript,
241 CodexOrchestratorReplayHarnessError
242> = {
243 driver: CodexAdapterV2.CODEX_DRIVER_KIND,
244 decodeTranscript: (transcript) =>
245 decodeCodexAppServerReplayTranscript(transcript).pipe(
246 Effect.mapError(
247 (cause) =>
248 new CodexReplayTranscriptDecodeError({
249 ...metadataFromTranscript(transcript),
250 cause,
251 }),
252 ),
253 ),
254 makeProviderAdapterRegistryLayer: (
255 transcript,
256 options: { readonly replayGate?: ProviderReplayGate } = {},
257 ) => {
258 return Layer.effectContext(
259 Effect.gen(function* () {
260 const replayGate = options.replayGate;
261 if (replayGate !== undefined) {
262 yield* Effect.addFinalizer(() => Effect.sync(() => replayGate.releaseAll()));
263 }
264 const driver = yield* CodexReplay.makeReplayDriver(
265 transcript,
266 replayGate === undefined
267 ? {}
268 : {
269 beforeEmitInbound: (entry) =>
270 Effect.promise((signal) => replayGate.beforeEmit(entry.label, signal)),
271 },
272 );
273 return yield* Layer.build(layer({ transcript, driver }));
274 }),
275 );
276 },
277};