@ai-sdk/harness-codex 1.0.93 → 1.0.95

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.
package/src/codex-auth.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type {
2
+ HarnessV1Authentication,
2
3
  HarnessV1RequestTransformation,
3
4
  HarnessV1RequestTransformationSources,
4
5
  } from '@ai-sdk/harness';
5
6
  import {
6
7
  createCredentialRequestTransformation,
7
8
  getAiGatewayAuthFromEnv,
9
+ isHarnessAuthenticationEnvironment,
8
10
  } from '@ai-sdk/harness/utils';
9
11
 
10
12
  const DEFAULT_AI_GATEWAY_BASE_URL = 'https://ai-gateway.vercel.sh/v1';
@@ -42,83 +44,48 @@ export function createCodexRequestTransformations({
42
44
 
43
45
  export type CodexResolvedAuthenticationMode = 'direct' | 'ai-gateway';
44
46
 
45
- export type CodexAuthenticationMode = CodexResolvedAuthenticationMode | 'auto';
46
-
47
- /**
48
- * @deprecated Passing an object to auth options is deprecated. Use a `CodexAuthenticationMode` string value ("auto" | "direct" | "ai-gateway") instead, and pass credentials via environment variables.
49
- */
50
- export type LegacyCodexAuthOptions = {
51
- readonly openaiCompatible?: {
52
- readonly apiKey?: string;
53
- readonly baseUrl?: string;
54
- readonly modelProviderName?: string;
55
- readonly queryParamsJson?: string;
56
- };
57
- readonly openai?: {
58
- readonly apiKey?: string;
59
- readonly baseUrl?: string;
60
- readonly organization?: string;
61
- readonly project?: string;
62
- };
63
- readonly gateway?: {
64
- readonly apiKey?: string;
65
- readonly baseUrl?: string;
66
- };
67
- };
68
-
69
- export type CodexAuthOptions = CodexAuthenticationMode | LegacyCodexAuthOptions;
47
+ export type CodexAuthenticationMode = HarnessV1Authentication;
70
48
 
71
49
  /**
72
50
  * Resolve the environment-variable blob the codex bridge needs. Precedence:
73
51
  *
74
- * 1. Explicit `auth.openaiCompatible` — pin to a custom OpenAI-compatible endpoint.
75
- * 2. Explicit `auth.openai` — pin to direct OpenAI auth.
76
- * 3. Explicit `auth.gateway` — pin to Vercel AI Gateway.
77
- * 4. Auto-detect from the host process env: gateway first
52
+ * 1. An explicit authentication mode pins the selected route.
53
+ * 2. Auto-detect from the host process env: gateway first
78
54
  * (`AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN`), then `CODEX_API_KEY` /
79
55
  * `OPENAI_API_KEY`.
80
56
  */
81
57
  export function resolveCodexEnv(
82
- auth: CodexAuthOptions | undefined,
58
+ auth: CodexAuthenticationMode | undefined,
83
59
  processEnv: Record<string, string | undefined> = process.env,
84
60
  ): Record<string, string> {
85
- const normalizedAuth = normalizeCodexAuthToLegacyAuth(auth);
86
-
87
- if (normalizedAuth?.openaiCompatible) {
88
- return pickOpenAICompatible(normalizedAuth.openaiCompatible, processEnv);
89
- }
90
- if (normalizedAuth?.openai) {
91
- return pickOpenAI({ explicit: normalizedAuth.openai, processEnv });
61
+ const suppliedEnvironment = isHarnessAuthenticationEnvironment(auth);
62
+ const authenticationEnvironment = suppliedEnvironment ? auth : processEnv;
63
+ if (auth === 'direct') {
64
+ return pickOpenAI({ processEnv: authenticationEnvironment });
92
65
  }
66
+
93
67
  const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
94
- env: processEnv,
68
+ env: authenticationEnvironment,
95
69
  });
96
- if (normalizedAuth?.gateway) {
97
- return pickGateway({
98
- explicit: normalizedAuth.gateway,
99
- gatewayAuthFromEnv,
100
- });
101
- }
102
- if (gatewayAuthFromEnv.apiKey) {
103
- return pickGateway({
104
- explicit: {},
105
- gatewayAuthFromEnv,
106
- });
70
+ if (auth === 'ai-gateway' || gatewayAuthFromEnv.apiKey) {
71
+ return pickGateway({ gatewayAuthFromEnv });
107
72
  }
108
- return pickOpenAI({ processEnv });
73
+ return pickOpenAI({ processEnv: authenticationEnvironment });
109
74
  }
110
75
 
111
76
  export function resolveCodexAuthenticationMode(
112
- auth: CodexAuthOptions | undefined,
77
+ auth: CodexAuthenticationMode | undefined,
113
78
  processEnv: Record<string, string | undefined> = process.env,
114
79
  ): CodexResolvedAuthenticationMode {
115
- if (typeof auth !== 'string' && auth?.openaiCompatible) {
116
- return 'direct';
80
+ if (isHarnessAuthenticationEnvironment(auth)) {
81
+ return getAiGatewayAuthFromEnv({ env: auth }).apiKey
82
+ ? 'ai-gateway'
83
+ : 'direct';
117
84
  }
118
- if (auth === 'direct' || (typeof auth !== 'string' && auth?.openai)) {
85
+ if (auth === 'direct') {
119
86
  return 'direct';
120
87
  }
121
- if (auth === 'ai-gateway' || (typeof auth !== 'string' && auth?.gateway)) {
88
+ if (auth === 'ai-gateway') {
122
89
  return 'ai-gateway';
123
90
  }
124
91
  return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey
@@ -126,76 +93,30 @@ export function resolveCodexAuthenticationMode(
126
93
  : 'direct';
127
94
  }
128
95
 
129
- function normalizeCodexAuthToLegacyAuth(
130
- auth: CodexAuthOptions | undefined,
131
- ): LegacyCodexAuthOptions | undefined {
132
- if (auth == null || auth === 'auto') {
133
- return undefined;
134
- }
135
- if (typeof auth === 'string') {
136
- switch (auth) {
137
- case 'direct':
138
- return { openai: {} };
139
- case 'ai-gateway':
140
- return { gateway: {} };
141
- default:
142
- return undefined;
143
- }
144
- }
145
-
146
- console.warn(
147
- '[codex] Passing an object to auth options is deprecated. Use a string mode ("auto" | "direct" | "ai-gateway") instead, and pass credentials via environment variables.',
148
- );
149
- return auth;
150
- }
151
-
152
- function pickOpenAICompatible(
153
- explicit: NonNullable<LegacyCodexAuthOptions['openaiCompatible']>,
154
- processEnv: Record<string, string | undefined>,
155
- ): Record<string, string> {
156
- const env: Record<string, string> = {};
157
- const apiKey =
158
- explicit.apiKey ?? processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
159
- if (apiKey) env.CODEX_API_KEY = apiKey;
160
- if (explicit.baseUrl) env.OPENAI_BASE_URL = explicit.baseUrl;
161
- if (explicit.modelProviderName)
162
- env.CODEX_MODEL_PROVIDER_NAME = explicit.modelProviderName;
163
- if (explicit.queryParamsJson)
164
- env.OPENAI_QUERY_PARAMS_JSON = explicit.queryParamsJson;
165
- return env;
166
- }
167
-
168
96
  function pickOpenAI({
169
- explicit,
170
97
  processEnv,
171
98
  }: {
172
- explicit?: NonNullable<LegacyCodexAuthOptions['openai']>;
173
99
  processEnv: Record<string, string | undefined>;
174
100
  }): Record<string, string> {
175
101
  const env: Record<string, string> = {};
176
- const apiKey =
177
- explicit?.apiKey ?? processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
102
+ const apiKey = processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
178
103
  if (apiKey) env.CODEX_API_KEY = apiKey;
179
- const baseUrl = explicit?.baseUrl ?? processEnv.OPENAI_BASE_URL;
104
+ const baseUrl = processEnv.OPENAI_BASE_URL;
180
105
  if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
181
- const organization = explicit?.organization ?? processEnv.OPENAI_ORGANIZATION;
106
+ const organization = processEnv.OPENAI_ORGANIZATION;
182
107
  if (organization) env.OPENAI_ORGANIZATION = organization;
183
- const project = explicit?.project ?? processEnv.OPENAI_PROJECT;
108
+ const project = processEnv.OPENAI_PROJECT;
184
109
  if (project) env.OPENAI_PROJECT = project;
185
110
  return env;
186
111
  }
187
112
 
188
113
  function pickGateway({
189
- explicit,
190
114
  gatewayAuthFromEnv,
191
115
  }: {
192
- explicit: NonNullable<LegacyCodexAuthOptions['gateway']>;
193
116
  gatewayAuthFromEnv: ReturnType<typeof getAiGatewayAuthFromEnv>;
194
117
  }): Record<string, string> {
195
- const apiKey = explicit.apiKey ?? gatewayAuthFromEnv.apiKey;
196
- const baseUrl = toCodexGatewayBaseUrl(
197
- explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl,
198
- );
118
+ const apiKey = gatewayAuthFromEnv.apiKey;
119
+ const baseUrl = toCodexGatewayBaseUrl(gatewayAuthFromEnv.baseUrl);
199
120
  const env: Record<string, string> = {};
200
121
  if (apiKey) {
201
122
  env.AI_GATEWAY_API_KEY = apiKey;
@@ -27,6 +27,7 @@ export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
27
27
  // The host sources the id from lifecycle state `data` cached from a prior
28
28
  // `agent.detach`.
29
29
  resumeThreadId: z.string().optional(),
30
+ restartThread: z.boolean().optional(),
30
31
  });
31
32
 
32
33
  export type StartMessage = z.infer<typeof startMessageSchema>;
@@ -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,
@@ -53,7 +54,7 @@ import {
53
54
  DEFAULT_OPENAI_BASE_URL,
54
55
  resolveCodexAuthenticationMode,
55
56
  resolveCodexEnv,
56
- type CodexAuthOptions,
57
+ type CodexAuthenticationMode,
57
58
  } from './codex-auth';
58
59
  import {
59
60
  outboundMessageSchema,
@@ -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
@@ -89,7 +85,7 @@ const DEFAULT_CODEX_MODEL = 'gpt-5.5';
89
85
  const CODEX_CLIENT_APP = `ai-sdk/harness-codex/${VERSION}`;
90
86
 
91
87
  export type CodexHarnessSettings = {
92
- readonly auth?: CodexAuthOptions;
88
+ readonly auth?: CodexAuthenticationMode;
93
89
  /**
94
90
  * Customizes each credential value before it is forwarded into a sandbox
95
91
  * process. This does not restrict which credentials the harness adapter can
@@ -110,6 +106,8 @@ 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
  /**
@@ -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 = startOpts.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;
@@ -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
@@ -928,16 +970,22 @@ function createSession({
928
970
  harnessId: 'codex',
929
971
  });
930
972
  }
931
- const turn = wireTurn({
932
- emit: promptOpts.emit,
933
- abortSignal: promptOpts.abortSignal,
934
- });
935
-
936
973
  const tools = (promptOpts.tools ?? []).map(t => ({
937
974
  name: t.name,
938
975
  description: t.description,
939
976
  inputSchema: t.inputSchema,
940
977
  }));
978
+ const { restartThread } = await synchronizeTurnConfiguration({
979
+ skills: promptOpts.skills,
980
+ instructions: promptOpts.instructions,
981
+ tools,
982
+ abortSignal: promptOpts.abortSignal,
983
+ });
984
+ const turn = wireTurn({
985
+ emit: promptOpts.emit,
986
+ abortSignal: promptOpts.abortSignal,
987
+ });
988
+
941
989
  let promptText = extractUserText(promptOpts.prompt);
942
990
  if (!initialPromptGuidanceApplied) {
943
991
  promptText = frameInitialPromptGuidance({
@@ -972,6 +1020,7 @@ function createSession({
972
1020
  ...(pendingResumeThreadId
973
1021
  ? { resumeThreadId: pendingResumeThreadId }
974
1022
  : {}),
1023
+ ...(restartThread ? { restartThread: true } : {}),
975
1024
  ...(debug ? { debug } : {}),
976
1025
  };
977
1026
  pendingResumeThreadId = undefined;
@@ -990,6 +1039,17 @@ function createSession({
990
1039
  harnessId: 'codex',
991
1040
  });
992
1041
  }
1042
+ const tools = (continueOpts.tools ?? []).map(t => ({
1043
+ name: t.name,
1044
+ description: t.description,
1045
+ inputSchema: t.inputSchema,
1046
+ }));
1047
+ const { restartThread } = await synchronizeTurnConfiguration({
1048
+ skills: continueOpts.skills,
1049
+ instructions: continueOpts.instructions,
1050
+ tools,
1051
+ abortSignal: continueOpts.abortSignal,
1052
+ });
993
1053
  const turn = wireTurn({
994
1054
  emit: continueOpts.emit,
995
1055
  abortSignal: continueOpts.abortSignal,
@@ -1021,11 +1081,7 @@ function createSession({
1021
1081
  * empty text block trips the Anthropic API's `cache_control` rule).
1022
1082
  */
1023
1083
  prompt: 'Continue.',
1024
- tools: (continueOpts.tools ?? []).map(t => ({
1025
- name: t.name,
1026
- description: t.description,
1027
- inputSchema: t.inputSchema,
1028
- })),
1084
+ tools,
1029
1085
  ...(continueOpts.responseFormat == null
1030
1086
  ? {}
1031
1087
  : { responseFormat: continueOpts.responseFormat }),
@@ -1039,6 +1095,7 @@ function createSession({
1039
1095
  ...(mcpServers == null ? {} : { mcpServers }),
1040
1096
  ...(permissionMode ? { permissionMode } : {}),
1041
1097
  ...(threadId ? { resumeThreadId: threadId } : {}),
1098
+ ...(restartThread ? { restartThread: true } : {}),
1042
1099
  ...(debug ? { debug } : {}),
1043
1100
  }),
1044
1101
  );
@@ -1074,6 +1131,12 @@ function createSession({
1074
1131
  specificationVersion: 'harness-v1',
1075
1132
  data: {
1076
1133
  ...(latestThreadId ? { threadId: latestThreadId } : {}),
1134
+ ...(latestTurnConfigurationFingerprint
1135
+ ? {
1136
+ turnConfigurationFingerprint:
1137
+ latestTurnConfigurationFingerprint,
1138
+ }
1139
+ : {}),
1077
1140
  ...(sandboxCredentialEnvironment == null
1078
1141
  ? {}
1079
1142
  : { sandboxCredentialEnvironment }),
@@ -1195,6 +1258,12 @@ function createSession({
1195
1258
  specificationVersion: 'harness-v1',
1196
1259
  data: {
1197
1260
  ...lifecycleData,
1261
+ ...(latestTurnConfigurationFingerprint
1262
+ ? {
1263
+ turnConfigurationFingerprint:
1264
+ latestTurnConfigurationFingerprint,
1265
+ }
1266
+ : {}),
1198
1267
  ...(sandboxCredentialEnvironment == null
1199
1268
  ? {}
1200
1269
  : { sandboxCredentialEnvironment }),
@@ -1224,6 +1293,12 @@ function createSession({
1224
1293
  specificationVersion: 'harness-v1',
1225
1294
  data: {
1226
1295
  ...(latestThreadId ? { threadId: latestThreadId } : {}),
1296
+ ...(latestTurnConfigurationFingerprint
1297
+ ? {
1298
+ turnConfigurationFingerprint:
1299
+ latestTurnConfigurationFingerprint,
1300
+ }
1301
+ : {}),
1227
1302
  ...(sandboxCredentialEnvironment == null
1228
1303
  ? {}
1229
1304
  : { sandboxCredentialEnvironment }),
@@ -1240,6 +1315,22 @@ function createSession({
1240
1315
  };
1241
1316
  }
1242
1317
 
1318
+ function fingerprintCodexTurnConfiguration({
1319
+ instructions,
1320
+ tools,
1321
+ }: {
1322
+ instructions: string | undefined;
1323
+ tools: ReadonlyArray<{
1324
+ name: string;
1325
+ description?: string;
1326
+ inputSchema: unknown;
1327
+ }>;
1328
+ }): string {
1329
+ return createHash('sha256')
1330
+ .update(JSON.stringify({ instructions: instructions ?? null, tools }))
1331
+ .digest('hex');
1332
+ }
1333
+
1243
1334
  /*
1244
1335
  * Frame host-tool relay guidance and the user's text so Codex treats the
1245
1336
  * prepended block as operating guidance rather than user prose. Applied only
package/src/index.ts CHANGED
@@ -10,4 +10,4 @@ export const codex = createCodex();
10
10
  export { createCodex } from './codex-harness';
11
11
  export { VERSION } from './version';
12
12
  export type { CodexHarnessSettings } from './codex-harness';
13
- export type { CodexAuthenticationMode, CodexAuthOptions } from './codex-auth';
13
+ export type { CodexAuthenticationMode } from './codex-auth';