@ai-sdk/harness-codex 1.0.94 → 1.0.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import {
4
4
  commonTool,
@@ -36,6 +36,7 @@ import {
36
36
  warnCredentialBrokeringUnavailable,
37
37
  waitForBridgeReady,
38
38
  writeSkills as writeHarnessSkills,
39
+ type WriteSkillsResult,
39
40
  } from '@ai-sdk/harness/utils';
40
41
  import {
41
42
  type Experimental_SandboxProcess,
@@ -66,11 +67,6 @@ import { VERSION } from './version';
66
67
  type CodexChannel = SandboxChannel<OutboundMessage, InboundMessage>;
67
68
  type CodexRespawnStrategy = 'replay' | 'rerun';
68
69
 
69
- type WriteSkillsResult = {
70
- readonly homeDir: string;
71
- readonly codexHomeDir: string;
72
- };
73
-
74
70
  /*
75
71
  * This intentionally is not the latest Codex model. Newer GPT-5.6 models use
76
72
  * Responses Lite, which does not expose their code-mode tools as callable
@@ -110,13 +106,15 @@ export type CodexHarnessSettings = {
110
106
  /**
111
107
  * OpenAI model id the underlying `codex` CLI should use. Leaving this unset
112
108
  * pins the adapter default (`DEFAULT_CODEX_MODEL`).
109
+ *
110
+ * @deprecated Use `model` on `HarnessAgent` instead.
113
111
  */
114
112
  readonly model?: string;
115
113
  /**
116
114
  * Reasoning effort for reasoning-capable models. Leaving this unset
117
115
  * defers to the CLI's default.
118
116
  */
119
- readonly reasoningEffort?: 'low' | 'medium' | 'high';
117
+ readonly reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
120
118
  /**
121
119
  * When `true`, allow the underlying runtime to use live web search.
122
120
  */
@@ -189,6 +187,7 @@ const codexBridgeCoordsSchema = z.object({
189
187
  */
190
188
  const codexResumeStateSchema = z.object({
191
189
  threadId: z.string().optional(),
190
+ turnConfigurationFingerprint: z.string().optional(),
192
191
  bridge: codexBridgeCoordsSchema.optional(),
193
192
  sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
194
193
  });
@@ -206,6 +205,7 @@ export function createCodex(
206
205
  lifecycleStateSchema: codexResumeStateSchema,
207
206
  getBootstrap: getCodexBootstrap,
208
207
  doStart: async startOpts => {
208
+ const model = settings.model ?? DEFAULT_CODEX_MODEL;
209
209
  if (startOpts.builtinToolFiltering != null) {
210
210
  throw new HarnessCapabilityUnsupportedError({
211
211
  message:
@@ -251,6 +251,7 @@ export function createCodex(
251
251
  isResume && typeof lifecycleState?.data === 'object'
252
252
  ? (lifecycleState.data as {
253
253
  threadId?: unknown;
254
+ turnConfigurationFingerprint?: unknown;
254
255
  bridge?: CodexBridgeCoords;
255
256
  sandboxCredentialEnvironment?: Record<string, string>;
256
257
  })
@@ -260,6 +261,10 @@ export function createCodex(
260
261
  typeof resumeThreadId === 'string' && resumeThreadId.length > 0
261
262
  ? resumeThreadId
262
263
  : undefined;
264
+ const turnConfigurationFingerprint =
265
+ typeof resumeData?.turnConfigurationFingerprint === 'string'
266
+ ? resumeData.turnConfigurationFingerprint
267
+ : undefined;
263
268
  const coords = resumeData?.bridge;
264
269
  const authenticationMode = resolveCodexAuthenticationMode(settings.auth);
265
270
  const resolvedAuthEnvironment = resolveCodexEnv(settings.auth);
@@ -315,6 +320,10 @@ export function createCodex(
315
320
  );
316
321
 
317
322
  const workDir = startOpts.sessionWorkDir;
323
+ const sandboxHomeDir = await resolveSandboxHomeDir({
324
+ sandbox: toolSafeSandboxSession,
325
+ abortSignal: startOpts.abortSignal,
326
+ });
318
327
  const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
319
328
  const bridgeStateDir = `${sessionDataDir}/bridge`;
320
329
  const cliShimDir = `${sessionDataDir}/codex`;
@@ -372,7 +381,7 @@ export function createCodex(
372
381
  cliShimPath,
373
382
  // The live bridge was spawned by another process; no process handle.
374
383
  proc: undefined,
375
- model: settings.model ?? DEFAULT_CODEX_MODEL,
384
+ model,
376
385
  reasoningEffort: settings.reasoningEffort,
377
386
  webSearch: settings.webSearch,
378
387
  codexConfig: settings.codexConfig,
@@ -387,6 +396,9 @@ export function createCodex(
387
396
  sandboxCredentialEnvironment,
388
397
  debug: startOpts.observability?.debug,
389
398
  permissionMode: startOpts.permissionMode,
399
+ sandbox: toolSafeSandboxSession,
400
+ sandboxHomeDir,
401
+ turnConfigurationFingerprint,
390
402
  });
391
403
  } catch {
392
404
  // Bridge no longer reachable — recover by respawning below.
@@ -432,25 +444,11 @@ export function createCodex(
432
444
  CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
433
445
  credentialForwarding: settings.credentialForwarding,
434
446
  });
435
- const codexSkillSetup =
436
- startOpts.skills && startOpts.skills.length > 0
437
- ? await writeCodexSkills({
438
- sandbox: toolSafeSandboxSession,
439
- skills: startOpts.skills,
440
- abortSignal: startOpts.abortSignal,
441
- })
442
- : undefined;
443
447
  const env = {
444
448
  ...forwardedAuthEnvironment,
445
449
  AI_SDK_HARNESS_CLIENT_APP: CODEX_CLIENT_APP,
446
450
  BRIDGE_CHANNEL_TOKEN: token,
447
451
  BRIDGE_WS_PORT: String(port),
448
- ...(codexSkillSetup
449
- ? {
450
- HOME: codexSkillSetup.homeDir,
451
- CODEX_HOME: codexSkillSetup.codexHomeDir,
452
- }
453
- : {}),
454
452
  ...(respawnStrategy === 'replay'
455
453
  ? { BRIDGE_REPLAY_FROM_DISK: '1' }
456
454
  : {}),
@@ -537,7 +535,7 @@ export function createCodex(
537
535
  channel,
538
536
  cliShimPath,
539
537
  proc,
540
- model: settings.model ?? DEFAULT_CODEX_MODEL,
538
+ model,
541
539
  reasoningEffort: settings.reasoningEffort,
542
540
  webSearch: settings.webSearch,
543
541
  codexConfig: settings.codexConfig,
@@ -552,6 +550,9 @@ export function createCodex(
552
550
  sandboxCredentialEnvironment,
553
551
  debug: startOpts.observability?.debug,
554
552
  permissionMode: startOpts.permissionMode,
553
+ sandbox: toolSafeSandboxSession,
554
+ sandboxHomeDir,
555
+ turnConfigurationFingerprint,
555
556
  });
556
557
  },
557
558
  };
@@ -624,22 +625,17 @@ async function resolveBridgeEndpoint({
624
625
 
625
626
  async function writeCodexSkills({
626
627
  sandbox,
628
+ sandboxHomeDir,
627
629
  skills,
628
630
  abortSignal,
629
631
  }: {
630
632
  sandbox: SandboxSession;
633
+ sandboxHomeDir: string;
631
634
  skills: ReadonlyArray<HarnessV1Skill>;
632
635
  abortSignal?: AbortSignal;
633
636
  }): Promise<WriteSkillsResult> {
634
- const homeDir = await resolveSandboxHomeDir({ sandbox, abortSignal });
635
- const codexHomeDir = path.posix.join(homeDir, '.codex');
636
- await sandbox.run({
637
- command: `mkdir -p ${shellQuote(codexHomeDir)}`,
638
- abortSignal,
639
- });
640
-
641
- const rootDir = path.posix.join(homeDir, '.agents', 'skills');
642
- await writeHarnessSkills({
637
+ const rootDir = path.posix.join(sandboxHomeDir, '.agents', 'skills');
638
+ return writeHarnessSkills({
643
639
  sandbox,
644
640
  rootDir,
645
641
  skills,
@@ -648,11 +644,6 @@ async function writeCodexSkills({
648
644
  invalidSkillFilePathMessage: ({ skillName, filePath }) =>
649
645
  `Invalid Codex skill file path for ${skillName}: ${filePath}`,
650
646
  });
651
-
652
- return {
653
- homeDir,
654
- codexHomeDir,
655
- };
656
647
  }
657
648
 
658
649
  function openWebSocket({
@@ -708,6 +699,9 @@ function createSession({
708
699
  sandboxCredentialEnvironment,
709
700
  debug,
710
701
  permissionMode,
702
+ sandbox,
703
+ sandboxHomeDir,
704
+ turnConfigurationFingerprint,
711
705
  }: {
712
706
  sessionId: string;
713
707
  channel: CodexChannel;
@@ -715,7 +709,7 @@ function createSession({
715
709
  /** Undefined on `attach` — the live bridge was spawned by another process. */
716
710
  proc: Experimental_SandboxProcess | undefined;
717
711
  model: string | undefined;
718
- reasoningEffort: 'low' | 'medium' | 'high' | undefined;
712
+ reasoningEffort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined;
719
713
  webSearch: boolean | undefined;
720
714
  codexConfig: Record<string, unknown> | undefined;
721
715
  mcpServers: Record<string, unknown> | undefined;
@@ -729,6 +723,9 @@ function createSession({
729
723
  sandboxCredentialEnvironment: Record<string, string> | undefined;
730
724
  debug: HarnessV1DebugConfig | undefined;
731
725
  permissionMode: HarnessV1PermissionMode | undefined;
726
+ sandbox: SandboxSession;
727
+ sandboxHomeDir: string;
728
+ turnConfigurationFingerprint: string | undefined;
732
729
  }): HarnessV1Session {
733
730
  let stopped = false;
734
731
  let stopPromise: Promise<void> | undefined;
@@ -755,10 +752,55 @@ function createSession({
755
752
  * can include a thread id even before this process has run a turn.
756
753
  */
757
754
  let latestThreadId = resumeThreadId;
755
+ let latestTurnConfigurationFingerprint = turnConfigurationFingerprint;
758
756
  channel.on('bridge-thread', msg => {
759
757
  latestThreadId = msg.threadId;
760
758
  });
761
759
 
760
+ const synchronizeTurnConfiguration = async ({
761
+ skills,
762
+ instructions,
763
+ tools,
764
+ abortSignal,
765
+ }: {
766
+ skills: ReadonlyArray<HarnessV1Skill>;
767
+ instructions: string | undefined;
768
+ tools: ReadonlyArray<{
769
+ name: string;
770
+ description?: string;
771
+ inputSchema: unknown;
772
+ }>;
773
+ abortSignal?: AbortSignal;
774
+ }): Promise<{ restartThread: boolean }> => {
775
+ const skillsResult = await writeCodexSkills({
776
+ sandbox,
777
+ sandboxHomeDir,
778
+ skills,
779
+ abortSignal,
780
+ });
781
+ const nextFingerprint = fingerprintCodexTurnConfiguration({
782
+ instructions,
783
+ tools,
784
+ });
785
+ const restartThread =
786
+ latestThreadId != null &&
787
+ (skillsResult.changed ||
788
+ (latestTurnConfigurationFingerprint != null &&
789
+ latestTurnConfigurationFingerprint !== nextFingerprint));
790
+ latestTurnConfigurationFingerprint = nextFingerprint;
791
+ if (restartThread) {
792
+ /*
793
+ * `codex exec resume` retains the native thread's original developer
794
+ * instructions and skill catalog. A fresh native thread is therefore
795
+ * required for replacement semantics. Host-tool guidance is framed
796
+ * again because that guidance also belongs to the new native thread.
797
+ */
798
+ initialPromptGuidanceApplied = false;
799
+ pendingResumeThreadId = undefined;
800
+ }
801
+ return { restartThread };
802
+ };
803
+
762
804
  /*
763
805
  * Wire the channel into one turn's worth of events and return the control
764
806
  * surface. Shared by `doPromptTurn` (which sends a `start` afterwards) and
@@ -916,7 +958,6 @@ function createSession({
916
958
  return {
917
959
  sessionId,
918
960
  isResume,
919
- modelId: model,
920
961
  doPromptTurn: async promptOpts => {
921
962
  if (
922
963
  promptOpts.responseFormat?.type === 'json' &&
@@ -928,16 +969,22 @@ function createSession({
928
969
  harnessId: 'codex',
929
970
  });
930
971
  }
931
- const turn = wireTurn({
932
- emit: promptOpts.emit,
933
- abortSignal: promptOpts.abortSignal,
934
- });
935
-
936
972
  const tools = (promptOpts.tools ?? []).map(t => ({
937
973
  name: t.name,
938
974
  description: t.description,
939
975
  inputSchema: t.inputSchema,
940
976
  }));
977
+ const { restartThread } = await synchronizeTurnConfiguration({
978
+ skills: promptOpts.skills,
979
+ instructions: promptOpts.instructions,
980
+ tools,
981
+ abortSignal: promptOpts.abortSignal,
982
+ });
983
+ const turn = wireTurn({
984
+ emit: promptOpts.emit,
985
+ abortSignal: promptOpts.abortSignal,
986
+ });
987
+
941
988
  let promptText = extractUserText(promptOpts.prompt);
942
989
  if (!initialPromptGuidanceApplied) {
943
990
  promptText = frameInitialPromptGuidance({
@@ -963,7 +1010,7 @@ function createSession({
963
1010
  ...(promptOpts.instructions
964
1011
  ? { instructions: promptOpts.instructions }
965
1012
  : {}),
966
- model,
1013
+ model: promptOpts.model ?? model,
967
1014
  reasoningEffort,
968
1015
  webSearch,
969
1016
  ...(codexConfig == null ? {} : { codexConfig }),
@@ -972,6 +1019,7 @@ function createSession({
972
1019
  ...(pendingResumeThreadId
973
1020
  ? { resumeThreadId: pendingResumeThreadId }
974
1021
  : {}),
1022
+ ...(restartThread ? { restartThread: true } : {}),
975
1023
  ...(debug ? { debug } : {}),
976
1024
  };
977
1025
  pendingResumeThreadId = undefined;
@@ -990,6 +1038,17 @@ function createSession({
990
1038
  harnessId: 'codex',
991
1039
  });
992
1040
  }
1041
+ const tools = (continueOpts.tools ?? []).map(t => ({
1042
+ name: t.name,
1043
+ description: t.description,
1044
+ inputSchema: t.inputSchema,
1045
+ }));
1046
+ const { restartThread } = await synchronizeTurnConfiguration({
1047
+ skills: continueOpts.skills,
1048
+ instructions: continueOpts.instructions,
1049
+ tools,
1050
+ abortSignal: continueOpts.abortSignal,
1051
+ });
993
1052
  const turn = wireTurn({
994
1053
  emit: continueOpts.emit,
995
1054
  abortSignal: continueOpts.abortSignal,
@@ -1021,24 +1080,21 @@ function createSession({
1021
1080
  * empty text block trips the Anthropic API's `cache_control` rule).
1022
1081
  */
1023
1082
  prompt: 'Continue.',
1024
- tools: (continueOpts.tools ?? []).map(t => ({
1025
- name: t.name,
1026
- description: t.description,
1027
- inputSchema: t.inputSchema,
1028
- })),
1083
+ tools,
1029
1084
  ...(continueOpts.responseFormat == null
1030
1085
  ? {}
1031
1086
  : { responseFormat: continueOpts.responseFormat }),
1032
1087
  ...(continueOpts.instructions
1033
1088
  ? { instructions: continueOpts.instructions }
1034
1089
  : {}),
1035
- model,
1090
+ model: continueOpts.model ?? model,
1036
1091
  reasoningEffort,
1037
1092
  webSearch,
1038
1093
  ...(codexConfig == null ? {} : { codexConfig }),
1039
1094
  ...(mcpServers == null ? {} : { mcpServers }),
1040
1095
  ...(permissionMode ? { permissionMode } : {}),
1041
1096
  ...(threadId ? { resumeThreadId: threadId } : {}),
1097
+ ...(restartThread ? { restartThread: true } : {}),
1042
1098
  ...(debug ? { debug } : {}),
1043
1099
  }),
1044
1100
  );
@@ -1074,6 +1130,12 @@ function createSession({
1074
1130
  specificationVersion: 'harness-v1',
1075
1131
  data: {
1076
1132
  ...(latestThreadId ? { threadId: latestThreadId } : {}),
1133
+ ...(latestTurnConfigurationFingerprint
1134
+ ? {
1135
+ turnConfigurationFingerprint:
1136
+ latestTurnConfigurationFingerprint,
1137
+ }
1138
+ : {}),
1077
1139
  ...(sandboxCredentialEnvironment == null
1078
1140
  ? {}
1079
1141
  : { sandboxCredentialEnvironment }),
@@ -1195,6 +1257,12 @@ function createSession({
1195
1257
  specificationVersion: 'harness-v1',
1196
1258
  data: {
1197
1259
  ...lifecycleData,
1260
+ ...(latestTurnConfigurationFingerprint
1261
+ ? {
1262
+ turnConfigurationFingerprint:
1263
+ latestTurnConfigurationFingerprint,
1264
+ }
1265
+ : {}),
1198
1266
  ...(sandboxCredentialEnvironment == null
1199
1267
  ? {}
1200
1268
  : { sandboxCredentialEnvironment }),
@@ -1224,6 +1292,12 @@ function createSession({
1224
1292
  specificationVersion: 'harness-v1',
1225
1293
  data: {
1226
1294
  ...(latestThreadId ? { threadId: latestThreadId } : {}),
1295
+ ...(latestTurnConfigurationFingerprint
1296
+ ? {
1297
+ turnConfigurationFingerprint:
1298
+ latestTurnConfigurationFingerprint,
1299
+ }
1300
+ : {}),
1227
1301
  ...(sandboxCredentialEnvironment == null
1228
1302
  ? {}
1229
1303
  : { sandboxCredentialEnvironment }),
@@ -1240,6 +1314,22 @@ function createSession({
1240
1314
  };
1241
1315
  }
1242
1316
 
1317
+ function fingerprintCodexTurnConfiguration({
1318
+ instructions,
1319
+ tools,
1320
+ }: {
1321
+ instructions: string | undefined;
1322
+ tools: ReadonlyArray<{
1323
+ name: string;
1324
+ description?: string;
1325
+ inputSchema: unknown;
1326
+ }>;
1327
+ }): string {
1328
+ return createHash('sha256')
1329
+ .update(JSON.stringify({ instructions: instructions ?? null, tools }))
1330
+ .digest('hex');
1331
+ }
1332
+
1243
1333
  /*
1244
1334
  * Frame host-tool relay guidance and the user's text so Codex treats the
1245
1335
  * prepended block as operating guidance rather than user prose. Applied only