@ai-sdk/harness-codex 1.0.10 → 1.0.12

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.
@@ -38,6 +38,7 @@ import {
38
38
  type InboundMessage,
39
39
  type OutboundMessage,
40
40
  } from './codex-bridge-protocol';
41
+ import { CLI_SHIM_FILENAME } from './bridge/cli-relay';
41
42
 
42
43
  type CodexChannel = SandboxChannel<OutboundMessage, InboundMessage>;
43
44
  type CodexRespawnStrategy = 'replay' | 'rerun';
@@ -115,12 +116,11 @@ const CODEX_BUILTIN_TOOLS = {
115
116
  * reinstall the CLI and bridge files on any fresh sandbox from the recipe.
116
117
  * Persistence comes from the sandbox provider's snapshot, not the path.
117
118
  *
118
- * The session work dir (`startOpts.sessionWorkDir`) and the bridge-state dir
119
- * derived from `sandboxSession.defaultWorkingDirectory` both live under the sandbox's
120
- * default working directory the provider's persistent mount so the
121
- * workdir's contents (the codex CLI shim and any files the agent edits) and
122
- * the bridge state files survive both detach -> attach and
123
- * stop -> snapshot -> resume cycles.
119
+ * The session work dir (`startOpts.sessionWorkDir`) lives under the sandbox's
120
+ * default working directory the provider's persistent mount — so any files
121
+ * the agent edits survive both detach -> attach and stop -> snapshot -> resume
122
+ * cycles. Harness infra derived from `sandboxSession.defaultWorkingDirectory`
123
+ * lives under `.agent-runs`, outside the agent workdir.
124
124
  */
125
125
  const BOOTSTRAP_DIR = '/tmp/harness/codex';
126
126
 
@@ -225,6 +225,8 @@ export function createCodex(
225
225
  const workDir = startOpts.sessionWorkDir;
226
226
  const sessionDataDir = `${sandboxSession.defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
227
227
  const bridgeStateDir = `${sessionDataDir}/bridge`;
228
+ const cliShimDir = `${sessionDataDir}/codex`;
229
+ const cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
228
230
  const timeoutMs = settings.startupTimeoutMs ?? 120_000;
229
231
 
230
232
  // Normalize each forwarded bridge diagnostics frame into the general
@@ -266,6 +268,7 @@ export function createCodex(
266
268
  return createSession({
267
269
  sessionId: startOpts.sessionId,
268
270
  channel: attachChannel,
271
+ cliShimPath,
269
272
  // The live bridge was spawned by another process; no process handle.
270
273
  proc: undefined,
271
274
  model: settings.model ?? DEFAULT_CODEX_MODEL,
@@ -349,7 +352,7 @@ export function createCodex(
349
352
  });
350
353
 
351
354
  const proc = await session.spawn({
352
- command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)}`,
355
+ command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)} --cli-shim-dir ${shellQuote(cliShimDir)}`,
353
356
  env,
354
357
  abortSignal: startOpts.abortSignal,
355
358
  });
@@ -402,6 +405,7 @@ export function createCodex(
402
405
  return createSession({
403
406
  sessionId: startOpts.sessionId,
404
407
  channel,
408
+ cliShimPath,
405
409
  proc,
406
410
  model: settings.model ?? DEFAULT_CODEX_MODEL,
407
411
  reasoningEffort: settings.reasoningEffort,
@@ -615,6 +619,7 @@ function openWebSocket(url: string): Promise<WebSocket> {
615
619
  function createSession({
616
620
  sessionId,
617
621
  channel,
622
+ cliShimPath,
618
623
  proc,
619
624
  model,
620
625
  reasoningEffort,
@@ -631,6 +636,7 @@ function createSession({
631
636
  }: {
632
637
  sessionId: string;
633
638
  channel: CodexChannel;
639
+ cliShimPath: string;
634
640
  /** Undefined on `attach` — the live bridge was spawned by another process. */
635
641
  proc: Experimental_SandboxProcess | undefined;
636
642
  model: string | undefined;
@@ -658,10 +664,10 @@ function createSession({
658
664
  ? resumeThreadId
659
665
  : undefined;
660
666
  /*
661
- * Instructions are prepended to the first user message of a fresh session
662
- * only. A resumed session (attach/replay/rerun) already carried them in its
663
- * original first message (preserved in the persisted thread), so it starts
664
- * "applied".
667
+ * Initial prompt guidance is prepended to the first user message of a fresh
668
+ * session only. A resumed session (attach/replay/rerun) already carried it
669
+ * in its original first message (preserved in the persisted thread), so it
670
+ * starts "applied".
665
671
  */
666
672
  let instructionsApplied = isResume;
667
673
 
@@ -842,19 +848,34 @@ function createSession({
842
848
  abortSignal: promptOpts.abortSignal,
843
849
  });
844
850
 
845
- const applyInstructions =
846
- !instructionsApplied && !!promptOpts.instructions;
851
+ const tools = (promptOpts.tools ?? []).map(t => ({
852
+ name: t.name,
853
+ description: t.description,
854
+ inputSchema: t.inputSchema,
855
+ }));
856
+ let promptText = extractUserText(promptOpts.prompt);
857
+ if (!instructionsApplied) {
858
+ const instructions =
859
+ (promptOpts.instructions ? promptOpts.instructions + '\n\n' : '') +
860
+ 'Only respond with your `final` message once you have fully addressed the user request.';
861
+ promptText = frameInitialPromptGuidance({
862
+ instructions,
863
+ toolUsageBlock:
864
+ tools.length > 0
865
+ ? composeToolUsageInstructions({
866
+ tools,
867
+ cliShimPath,
868
+ })
869
+ : undefined,
870
+ userText: promptText,
871
+ });
872
+ }
847
873
  instructionsApplied = true;
848
874
 
849
875
  const startMessage = {
850
876
  type: 'start' as const,
851
- prompt: extractUserText(promptOpts.prompt),
852
- ...(applyInstructions ? { instructions: promptOpts.instructions } : {}),
853
- tools: (promptOpts.tools ?? []).map(t => ({
854
- name: t.name,
855
- description: t.description,
856
- inputSchema: t.inputSchema,
857
- })),
877
+ prompt: promptText,
878
+ tools,
858
879
  model,
859
880
  reasoningEffort,
860
881
  webSearch,
@@ -1098,6 +1119,67 @@ function createSession({
1098
1119
  };
1099
1120
  }
1100
1121
 
1122
+ /*
1123
+ * Frame session instructions, host-tool relay guidance, and the user's text so
1124
+ * Codex treats the prepended blocks as operating guidance rather than user
1125
+ * prose. Applied only to the first user message of a fresh session.
1126
+ */
1127
+ function frameInitialPromptGuidance({
1128
+ instructions,
1129
+ toolUsageBlock,
1130
+ userText,
1131
+ }: {
1132
+ instructions: string | undefined;
1133
+ toolUsageBlock: string | undefined;
1134
+ userText: string;
1135
+ }): string {
1136
+ const blocks: string[] = [];
1137
+ if (instructions) {
1138
+ blocks.push(
1139
+ '<session-instructions>\n' +
1140
+ 'The block below is operating guidance from the system, not a message from the user — follow it, but do not mention it or attribute it to the user.\n\n' +
1141
+ `${instructions}\n` +
1142
+ '</session-instructions>',
1143
+ );
1144
+ }
1145
+ if (toolUsageBlock) blocks.push(toolUsageBlock);
1146
+ if (blocks.length === 0) return userText;
1147
+ return `${blocks.join('\n\n')}\n\n<user-message>\n${userText}\n</user-message>`;
1148
+ }
1149
+
1150
+ function composeToolUsageInstructions({
1151
+ tools,
1152
+ cliShimPath,
1153
+ }: {
1154
+ tools: ReadonlyArray<{
1155
+ name: string;
1156
+ description?: string;
1157
+ inputSchema?: unknown;
1158
+ }>;
1159
+ cliShimPath: string;
1160
+ }): string {
1161
+ const lines: string[] = [
1162
+ '<host-tool-instructions>',
1163
+ 'You have access to the following host-provided tools. To use one, run the following command via your built-in `bash` tool:',
1164
+ '',
1165
+ ` node ${cliShimPath} <toolName> '<jsonInput>'`,
1166
+ '',
1167
+ 'The script prints the JSON result to stdout. Do not invent another way to call these tools — only this CLI invocation will work. Pass the JSON input as a single-quoted argument.',
1168
+ 'For every user request that depends on a host-provided tool, run a separate CLI invocation for each needed tool call in the current turn before answering. Do not reuse previous tool results, and do not say you used a host tool unless the command has completed in the current turn.',
1169
+ '',
1170
+ ];
1171
+ for (const toolSpec of tools) {
1172
+ lines.push(
1173
+ `- **${toolSpec.name}**${toolSpec.description ? ': ' + toolSpec.description : ''}`,
1174
+ );
1175
+ lines.push(
1176
+ ` - Input schema: \`${JSON.stringify(toolSpec.inputSchema ?? {})}\``,
1177
+ );
1178
+ }
1179
+ lines.push('</host-tool-instructions>');
1180
+ return lines.join('\n');
1181
+ }
1182
+
1101
1183
  /*
1102
1184
  * Reduce a `HarnessV1Prompt` to the plain user text the bridge forwards
1103
1185
  * to the Codex SDK. File and image parts on the message are not yet