@ai-sdk/harness-codex 1.0.9 → 1.0.11
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/CHANGELOG.md +13 -0
- package/dist/bridge/index.mjs +24 -65
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +76 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/cli-relay.ts +6 -39
- package/src/bridge/index.ts +33 -57
- package/src/codex-bridge-protocol.ts +0 -1
- package/src/codex-harness.ts +102 -20
package/src/bridge/index.ts
CHANGED
|
@@ -18,13 +18,12 @@ import {
|
|
|
18
18
|
import type { HarnessV1BuiltinToolName } from '@ai-sdk/harness';
|
|
19
19
|
import type { StartMessage } from '../codex-bridge-protocol';
|
|
20
20
|
import { randomUUID } from 'node:crypto';
|
|
21
|
-
import { writeFile } from 'node:fs/promises';
|
|
21
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
22
22
|
import { createServer, type Server } from 'node:http';
|
|
23
23
|
// Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
|
|
24
24
|
import {
|
|
25
25
|
CLI_SHIM_FILENAME,
|
|
26
26
|
buildCliShimScript,
|
|
27
|
-
composeToolUsageInstructions,
|
|
28
27
|
isToolRelayCommand,
|
|
29
28
|
} from './cli-relay';
|
|
30
29
|
import { argv, env as procEnv, stdout } from 'node:process';
|
|
@@ -63,14 +62,15 @@ function toCommonName(nativeName: string): HarnessV1BuiltinToolName | string {
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
const args = parseArgs(argv.slice(2));
|
|
66
|
-
const workdir = args.workdir;
|
|
67
|
-
const bridgeStateDir =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
const workdir = requireArg({ value: args.workdir, name: '--workdir' });
|
|
66
|
+
const bridgeStateDir = requireArg({
|
|
67
|
+
value: args.bridgeStateDir,
|
|
68
|
+
name: '--bridge-state-dir',
|
|
69
|
+
});
|
|
70
|
+
const cliShimDir = requireArg({
|
|
71
|
+
value: args.cliShimDir,
|
|
72
|
+
name: '--cli-shim-dir',
|
|
73
|
+
});
|
|
74
74
|
const bootstrapDir = args.bootstrapDir ?? workdir;
|
|
75
75
|
|
|
76
76
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -114,10 +114,9 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
114
114
|
* Until that's fixed, host tools are made available to the model via a
|
|
115
115
|
* separate CLI-relay workaround (see `./cli-relay.ts`). The MCP server
|
|
116
116
|
* config below is kept so that the day codex starts exposing MCP tools
|
|
117
|
-
* properly, host tools work both ways.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* and can be removed once the upstream bug is fixed.
|
|
117
|
+
* properly, host tools work both ways. Writing the shim here, adding matching
|
|
118
|
+
* prompt guidance in the host adapter, and filtering the shim command below
|
|
119
|
+
* implement the workaround and can be removed once the upstream bug is fixed.
|
|
121
120
|
*/
|
|
122
121
|
const mcpServers: Record<string, unknown> = {};
|
|
123
122
|
let relay: { port: number; close(): void } | undefined;
|
|
@@ -147,7 +146,8 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
147
146
|
},
|
|
148
147
|
};
|
|
149
148
|
// Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
|
|
150
|
-
cliShimPath = `${
|
|
149
|
+
cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
|
|
150
|
+
await mkdir(cliShimDir, { recursive: true });
|
|
151
151
|
await writeFile(
|
|
152
152
|
cliShimPath,
|
|
153
153
|
buildCliShimScript({ relayPort: relay.port, relayToken }),
|
|
@@ -212,18 +212,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
212
212
|
|
|
213
213
|
emit({ type: 'stream-start' });
|
|
214
214
|
|
|
215
|
-
const userMessage =
|
|
216
|
-
text: start.prompt,
|
|
217
|
-
instructions: start.instructions,
|
|
218
|
-
// Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
|
|
219
|
-
toolUsageBlock:
|
|
220
|
-
cliShimPath && start.tools && start.tools.length > 0
|
|
221
|
-
? composeToolUsageInstructions({
|
|
222
|
-
tools: start.tools,
|
|
223
|
-
cliShimPath,
|
|
224
|
-
})
|
|
225
|
-
: undefined,
|
|
226
|
-
});
|
|
215
|
+
const userMessage = start.prompt;
|
|
227
216
|
let turnUsage: Record<string, unknown> | undefined;
|
|
228
217
|
const textByItem = new Map<string, string>();
|
|
229
218
|
const reasoningByItem = new Map<string, string>();
|
|
@@ -523,36 +512,6 @@ function defaultUsage(): Record<string, unknown> {
|
|
|
523
512
|
};
|
|
524
513
|
}
|
|
525
514
|
|
|
526
|
-
function composeUserMessage({
|
|
527
|
-
text,
|
|
528
|
-
instructions,
|
|
529
|
-
toolUsageBlock,
|
|
530
|
-
}: {
|
|
531
|
-
text: string;
|
|
532
|
-
instructions: string | undefined;
|
|
533
|
-
toolUsageBlock: string | undefined;
|
|
534
|
-
}): string {
|
|
535
|
-
const blocks: string[] = [];
|
|
536
|
-
/*
|
|
537
|
-
* Frame instructions as system-provided operating guidance, not something
|
|
538
|
-
* the user wrote, so the agent does not echo the prepended text back as if
|
|
539
|
-
* the user had asked for it. Only present on the first user message of a
|
|
540
|
-
* fresh session (the host gates it), so the matching `<user-message>` fence
|
|
541
|
-
* is added only when instructions are present too.
|
|
542
|
-
*/
|
|
543
|
-
if (instructions) {
|
|
544
|
-
blocks.push(
|
|
545
|
-
'<session-instructions>\n' +
|
|
546
|
-
'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' +
|
|
547
|
-
`${instructions}\n` +
|
|
548
|
-
'</session-instructions>',
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
if (toolUsageBlock) blocks.push(toolUsageBlock);
|
|
552
|
-
blocks.push(instructions ? `<user-message>\n${text}\n</user-message>` : text);
|
|
553
|
-
return blocks.join('\n\n');
|
|
554
|
-
}
|
|
555
|
-
|
|
556
515
|
/**
|
|
557
516
|
* Tool relay — HTTP server on 127.0.0.1:0 with bearer-token auth. The MCP
|
|
558
517
|
* stdio shim spawned by codex POSTs each tool invocation here; the relay
|
|
@@ -657,11 +616,13 @@ function parseArgs(args: string[]): {
|
|
|
657
616
|
workdir?: string;
|
|
658
617
|
bridgeStateDir?: string;
|
|
659
618
|
bootstrapDir?: string;
|
|
619
|
+
cliShimDir?: string;
|
|
660
620
|
} {
|
|
661
621
|
const out: {
|
|
662
622
|
workdir?: string;
|
|
663
623
|
bridgeStateDir?: string;
|
|
664
624
|
bootstrapDir?: string;
|
|
625
|
+
cliShimDir?: string;
|
|
665
626
|
} = {};
|
|
666
627
|
for (let i = 0; i < args.length; i++) {
|
|
667
628
|
if (args[i] === '--workdir' && i + 1 < args.length) {
|
|
@@ -670,6 +631,8 @@ function parseArgs(args: string[]): {
|
|
|
670
631
|
out.bridgeStateDir = args[++i];
|
|
671
632
|
} else if (args[i] === '--bootstrap-dir' && i + 1 < args.length) {
|
|
672
633
|
out.bootstrapDir = args[++i];
|
|
634
|
+
} else if (args[i] === '--cli-shim-dir' && i + 1 < args.length) {
|
|
635
|
+
out.cliShimDir = args[++i];
|
|
673
636
|
}
|
|
674
637
|
}
|
|
675
638
|
return out;
|
|
@@ -686,3 +649,16 @@ function emitFatal(message: string): never {
|
|
|
686
649
|
stdout.write(JSON.stringify({ type: 'bridge-fatal', message }) + '\n');
|
|
687
650
|
process.exit(1);
|
|
688
651
|
}
|
|
652
|
+
|
|
653
|
+
function requireArg({
|
|
654
|
+
value,
|
|
655
|
+
name,
|
|
656
|
+
}: {
|
|
657
|
+
value: string | undefined;
|
|
658
|
+
name: string;
|
|
659
|
+
}): string {
|
|
660
|
+
if (!value) {
|
|
661
|
+
emitFatal(`Missing ${name} argument.`);
|
|
662
|
+
}
|
|
663
|
+
return value;
|
|
664
|
+
}
|
|
@@ -17,7 +17,6 @@ export const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
|
|
|
17
17
|
export type OutboundMessage = z.infer<typeof outboundMessageSchema>;
|
|
18
18
|
|
|
19
19
|
export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
|
|
20
|
-
instructions: z.string().optional(),
|
|
21
20
|
reasoningEffort: z.enum(['low', 'medium', 'high']).optional(),
|
|
22
21
|
webSearch: z.boolean().optional(),
|
|
23
22
|
// Resume signal. When supplied, the bridge calls
|
package/src/codex-harness.ts
CHANGED
|
@@ -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`)
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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
|
-
*
|
|
662
|
-
* only. A resumed session (attach/replay/rerun) already carried
|
|
663
|
-
* original first message (preserved in the persisted thread), so it
|
|
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
|
|
846
|
-
|
|
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:
|
|
852
|
-
|
|
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
|