20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -61,6 +61,12 @@ importers:
|
|||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.45.0
|
specifier: ^8.45.0
|
||||||
version: 8.45.0(eslint@9.36.0)(typescript@5.9.2)
|
version: 8.45.0(eslint@9.36.0)(typescript@5.9.2)
|
||||||
|
zod:
|
||||||
|
specifier: ^3.24.2
|
||||||
|
version: 3.25.76
|
||||||
|
zod-to-json-schema:
|
||||||
|
specifier: ^3.24.6
|
||||||
|
version: 3.24.6(zod@3.25.76)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -2175,6 +2181,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
zod-to-json-schema@3.24.6:
|
||||||
|
resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.24.1
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@babel/code-frame@7.27.1':
|
'@babel/code-frame@7.27.1':
|
||||||
@@ -4492,3 +4506,9 @@ snapshots:
|
|||||||
yn@3.1.1: {}
|
yn@3.1.1: {}
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
zod-to-json-schema@3.24.6(zod@3.25.76):
|
||||||
|
dependencies:
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
|
zod@3.25.76: {}
|
||||||
|
|||||||
@@ -50,6 +50,41 @@ for await (const event of events) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Structured output
|
||||||
|
|
||||||
|
Provide a JSON schema per turn to have Codex respond with structured JSON. Pass schemas as
|
||||||
|
plain JavaScript objects.
|
||||||
|
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
summary: { type: "string" },
|
||||||
|
status: { type: "string", enum: ["ok", "action_required"] },
|
||||||
|
},
|
||||||
|
required: ["summary", "status"],
|
||||||
|
additionalProperties: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const turn = await thread.run("Summarize repository status", { outputSchema: schema });
|
||||||
|
console.log(turn.finalResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also create JSON schemas for Zod types using the `zod-to-json-schema` package and setting the `target` to `"openAi"`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const schema = z.object({
|
||||||
|
summary: z.string(),
|
||||||
|
status: z.enum(["ok", "action_required"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const turn = await thread.run("Summarize repository status", {
|
||||||
|
outputSchema: zodToJsonSchema(schema, { target: "openAi" }),
|
||||||
|
});
|
||||||
|
console.log(turn.finalResponse);
|
||||||
|
```
|
||||||
|
|
||||||
### Resuming an existing thread
|
### Resuming an existing thread
|
||||||
|
|
||||||
Threads are persisted in `~/.codex/sessions`. If you lose the in-memory `Thread` object, reconstruct it with `resumeThread()` and keep going.
|
Threads are persisted in `~/.codex/sessions`. If you lose the in-memory `Thread` object, reconstruct it with `resumeThread()` and keep going.
|
||||||
@@ -70,4 +105,3 @@ const thread = codex.startThread({
|
|||||||
skipGitRepoCheck: true,
|
skipGitRepoCheck: true,
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"typescript-eslint": "^8.45.0"
|
"typescript-eslint": "^8.45.0",
|
||||||
|
"zod": "^3.24.2",
|
||||||
|
"zod-to-json-schema": "^3.24.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,9 @@ import { stdin as input, stdout as output } from "node:process";
|
|||||||
|
|
||||||
import { Codex } from "@openai/codex-sdk";
|
import { Codex } from "@openai/codex-sdk";
|
||||||
import type { ThreadEvent, ThreadItem } from "@openai/codex-sdk";
|
import type { ThreadEvent, ThreadItem } from "@openai/codex-sdk";
|
||||||
import path from "node:path";
|
import { codexPathOverride } from "./helpers.ts";
|
||||||
|
|
||||||
const codexPathOverride =
|
const codex = new Codex({ codexPathOverride: codexPathOverride() });
|
||||||
process.env.CODEX_EXECUTABLE ??
|
|
||||||
path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
|
|
||||||
|
|
||||||
const codex = new Codex({ codexPathOverride });
|
|
||||||
const thread = codex.startThread();
|
const thread = codex.startThread();
|
||||||
const rl = createInterface({ input, output });
|
const rl = createInterface({ input, output });
|
||||||
|
|
||||||
|
|||||||
6
sdk/typescript/samples/helpers.ts
Normal file
6
sdk/typescript/samples/helpers.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export function codexPathOverride() {
|
||||||
|
return process.env.CODEX_EXECUTABLE ??
|
||||||
|
path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
|
||||||
|
}
|
||||||
22
sdk/typescript/samples/structured_output.ts
Executable file
22
sdk/typescript/samples/structured_output.ts
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env -S NODE_NO_WARNINGS=1 pnpm ts-node-esm --files
|
||||||
|
|
||||||
|
import { Codex } from "@openai/codex-sdk";
|
||||||
|
|
||||||
|
import { codexPathOverride } from "./helpers.ts";
|
||||||
|
|
||||||
|
const codex = new Codex({ codexPathOverride: codexPathOverride() });
|
||||||
|
|
||||||
|
const thread = codex.startThread();
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
summary: { type: "string" },
|
||||||
|
status: { type: "string", enum: ["ok", "action_required"] },
|
||||||
|
},
|
||||||
|
required: ["summary", "status"],
|
||||||
|
additionalProperties: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const turn = await thread.run("Summarize repository status", { outputSchema: schema });
|
||||||
|
console.log(turn.finalResponse);
|
||||||
19
sdk/typescript/samples/structured_output_zod.ts
Executable file
19
sdk/typescript/samples/structured_output_zod.ts
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env -S NODE_NO_WARNINGS=1 pnpm ts-node-esm --files
|
||||||
|
|
||||||
|
import { Codex } from "@openai/codex-sdk";
|
||||||
|
import { codexPathOverride } from "./helpers.ts";
|
||||||
|
import z from "zod";
|
||||||
|
import zodToJsonSchema from "zod-to-json-schema";
|
||||||
|
|
||||||
|
const codex = new Codex({ codexPathOverride: codexPathOverride() });
|
||||||
|
const thread = codex.startThread();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
summary: z.string(),
|
||||||
|
status: z.enum(["ok", "action_required"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const turn = await thread.run("Summarize repository status", {
|
||||||
|
outputSchema: zodToJsonSchema(schema, { target: "openAi" }),
|
||||||
|
});
|
||||||
|
console.log(turn.finalResponse);
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
import path from "node:path";
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import { SandboxMode } from "./threadOptions";
|
import { SandboxMode } from "./threadOptions";
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
export type CodexExecArgs = {
|
export type CodexExecArgs = {
|
||||||
input: string;
|
input: string;
|
||||||
@@ -20,6 +19,8 @@ export type CodexExecArgs = {
|
|||||||
workingDirectory?: string;
|
workingDirectory?: string;
|
||||||
// --skip-git-repo-check
|
// --skip-git-repo-check
|
||||||
skipGitRepoCheck?: boolean;
|
skipGitRepoCheck?: boolean;
|
||||||
|
// --output-schema
|
||||||
|
outputSchemaFile?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CodexExec {
|
export class CodexExec {
|
||||||
@@ -47,6 +48,10 @@ export class CodexExec {
|
|||||||
commandArgs.push("--skip-git-repo-check");
|
commandArgs.push("--skip-git-repo-check");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.outputSchemaFile) {
|
||||||
|
commandArgs.push("--output-schema", args.outputSchemaFile);
|
||||||
|
}
|
||||||
|
|
||||||
if (args.threadId) {
|
if (args.threadId) {
|
||||||
commandArgs.push("resume", args.threadId);
|
commandArgs.push("resume", args.threadId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,3 +31,4 @@ export { Codex } from "./codex";
|
|||||||
export type { CodexOptions } from "./codexOptions";
|
export type { CodexOptions } from "./codexOptions";
|
||||||
|
|
||||||
export type { ThreadOptions, ApprovalMode, SandboxMode } from "./threadOptions";
|
export type { ThreadOptions, ApprovalMode, SandboxMode } from "./threadOptions";
|
||||||
|
export type { TurnOptions } from "./turnOptions";
|
||||||
|
|||||||
41
sdk/typescript/src/outputSchemaFile.ts
Normal file
41
sdk/typescript/src/outputSchemaFile.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export type OutputSchemaFile = {
|
||||||
|
schemaPath?: string;
|
||||||
|
cleanup: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createOutputSchemaFile(schema: unknown): Promise<OutputSchemaFile> {
|
||||||
|
if (schema === undefined) {
|
||||||
|
return { cleanup: async () => {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isJsonObject(schema)) {
|
||||||
|
throw new Error("outputSchema must be a plain JSON object");
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
|
||||||
|
const schemaPath = path.join(schemaDir, "schema.json");
|
||||||
|
const cleanup = async () => {
|
||||||
|
try {
|
||||||
|
await fs.rm(schemaDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// suppress
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
|
||||||
|
return { schemaPath, cleanup };
|
||||||
|
} catch (error) {
|
||||||
|
await cleanup();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import { ThreadEvent, ThreadError, Usage } from "./events";
|
|||||||
import { CodexExec } from "./exec";
|
import { CodexExec } from "./exec";
|
||||||
import { ThreadItem } from "./items";
|
import { ThreadItem } from "./items";
|
||||||
import { ThreadOptions } from "./threadOptions";
|
import { ThreadOptions } from "./threadOptions";
|
||||||
|
import { TurnOptions } from "./turnOptions";
|
||||||
|
import { createOutputSchemaFile } from "./outputSchemaFile";
|
||||||
|
|
||||||
/** Completed turn. */
|
/** Completed turn. */
|
||||||
export type Turn = {
|
export type Turn = {
|
||||||
@@ -51,11 +53,15 @@ export class Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Provides the input to the agent and streams events as they are produced during the turn. */
|
/** Provides the input to the agent and streams events as they are produced during the turn. */
|
||||||
async runStreamed(input: string): Promise<StreamedTurn> {
|
async runStreamed(input: string, turnOptions: TurnOptions = {}): Promise<StreamedTurn> {
|
||||||
return { events: this.runStreamedInternal(input) };
|
return { events: this.runStreamedInternal(input, turnOptions) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async *runStreamedInternal(input: string): AsyncGenerator<ThreadEvent> {
|
private async *runStreamedInternal(
|
||||||
|
input: string,
|
||||||
|
turnOptions: TurnOptions = {},
|
||||||
|
): AsyncGenerator<ThreadEvent> {
|
||||||
|
const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
|
||||||
const options = this._threadOptions;
|
const options = this._threadOptions;
|
||||||
const generator = this._exec.run({
|
const generator = this._exec.run({
|
||||||
input,
|
input,
|
||||||
@@ -66,24 +72,29 @@ export class Thread {
|
|||||||
sandboxMode: options?.sandboxMode,
|
sandboxMode: options?.sandboxMode,
|
||||||
workingDirectory: options?.workingDirectory,
|
workingDirectory: options?.workingDirectory,
|
||||||
skipGitRepoCheck: options?.skipGitRepoCheck,
|
skipGitRepoCheck: options?.skipGitRepoCheck,
|
||||||
|
outputSchemaFile: schemaPath,
|
||||||
});
|
});
|
||||||
for await (const item of generator) {
|
try {
|
||||||
let parsed: ThreadEvent;
|
for await (const item of generator) {
|
||||||
try {
|
let parsed: ThreadEvent;
|
||||||
parsed = JSON.parse(item) as ThreadEvent;
|
try {
|
||||||
} catch (error) {
|
parsed = JSON.parse(item) as ThreadEvent;
|
||||||
throw new Error(`Failed to parse item: ${item}`, { cause: error });
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to parse item: ${item}`, { cause: error });
|
||||||
|
}
|
||||||
|
if (parsed.type === "thread.started") {
|
||||||
|
this._id = parsed.thread_id;
|
||||||
|
}
|
||||||
|
yield parsed;
|
||||||
}
|
}
|
||||||
if (parsed.type === "thread.started") {
|
} finally {
|
||||||
this._id = parsed.thread_id;
|
await cleanup();
|
||||||
}
|
|
||||||
yield parsed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides the input to the agent and returns the completed turn. */
|
/** Provides the input to the agent and returns the completed turn. */
|
||||||
async run(input: string): Promise<Turn> {
|
async run(input: string, turnOptions: TurnOptions = {}): Promise<Turn> {
|
||||||
const generator = this.runStreamedInternal(input);
|
const generator = this.runStreamedInternal(input, turnOptions);
|
||||||
const items: ThreadItem[] = [];
|
const items: ThreadItem[] = [];
|
||||||
let finalResponse: string = "";
|
let finalResponse: string = "";
|
||||||
let usage: Usage | null = null;
|
let usage: Usage | null = null;
|
||||||
|
|||||||
4
sdk/typescript/src/turnOptions.ts
Normal file
4
sdk/typescript/src/turnOptions.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type TurnOptions = {
|
||||||
|
/** JSON schema describing the expected agent output. */
|
||||||
|
outputSchema?: unknown;
|
||||||
|
};
|
||||||
@@ -46,6 +46,9 @@ export type ResponsesApiRequest = {
|
|||||||
role: string;
|
role: string;
|
||||||
content?: Array<{ type: string; text: string }>;
|
content?: Array<{ type: string; text: string }>;
|
||||||
}>;
|
}>;
|
||||||
|
text?: {
|
||||||
|
format?: Record<string, unknown>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RecordedRequest = {
|
export type RecordedRequest = {
|
||||||
|
|||||||
@@ -222,6 +222,63 @@ describe("Codex", () => {
|
|||||||
await close();
|
await close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("writes output schema to a temporary file and forwards it", async () => {
|
||||||
|
const { url, close, requests } = await startResponsesTestProxy({
|
||||||
|
statusCode: 200,
|
||||||
|
responseBodies: [
|
||||||
|
sse(
|
||||||
|
responseStarted("response_1"),
|
||||||
|
assistantMessage("Structured response", "item_1"),
|
||||||
|
responseCompleted("response_1"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { args: spawnArgs, restore } = codexExecSpy();
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
answer: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["answer"],
|
||||||
|
additionalProperties: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
|
||||||
|
|
||||||
|
const thread = client.startThread();
|
||||||
|
await thread.run("structured", { outputSchema: schema });
|
||||||
|
|
||||||
|
expect(requests.length).toBeGreaterThanOrEqual(1);
|
||||||
|
const payload = requests[0];
|
||||||
|
expect(payload).toBeDefined();
|
||||||
|
const text = payload!.json.text;
|
||||||
|
expect(text).toBeDefined();
|
||||||
|
expect(text?.format).toEqual({
|
||||||
|
name: "codex_output_schema",
|
||||||
|
type: "json_schema",
|
||||||
|
strict: true,
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const commandArgs = spawnArgs[0];
|
||||||
|
expect(commandArgs).toBeDefined();
|
||||||
|
const schemaFlagIndex = commandArgs!.indexOf("--output-schema");
|
||||||
|
expect(schemaFlagIndex).toBeGreaterThan(-1);
|
||||||
|
const schemaPath = commandArgs![schemaFlagIndex + 1];
|
||||||
|
expect(typeof schemaPath).toBe("string");
|
||||||
|
if (typeof schemaPath !== "string") {
|
||||||
|
throw new Error("--output-schema flag missing path argument");
|
||||||
|
}
|
||||||
|
expect(fs.existsSync(schemaPath)).toBe(false);
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
});
|
||||||
it("runs in provided working directory", async () => {
|
it("runs in provided working directory", async () => {
|
||||||
const { url, close } = await startResponsesTestProxy({
|
const { url, close } = await startResponsesTestProxy({
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
|||||||
@@ -157,6 +157,50 @@ describe("Codex", () => {
|
|||||||
await close();
|
await close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies output schema turn options when streaming", async () => {
|
||||||
|
const { url, close, requests } = await startResponsesTestProxy({
|
||||||
|
statusCode: 200,
|
||||||
|
responseBodies: [
|
||||||
|
sse(
|
||||||
|
responseStarted("response_1"),
|
||||||
|
assistantMessage("Structured response", "item_1"),
|
||||||
|
responseCompleted("response_1"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
answer: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["answer"],
|
||||||
|
additionalProperties: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
|
||||||
|
|
||||||
|
const thread = client.startThread();
|
||||||
|
const streamed = await thread.runStreamed("structured", { outputSchema: schema });
|
||||||
|
await drainEvents(streamed.events);
|
||||||
|
|
||||||
|
expect(requests.length).toBeGreaterThanOrEqual(1);
|
||||||
|
const payload = requests[0];
|
||||||
|
expect(payload).toBeDefined();
|
||||||
|
const text = payload!.json.text;
|
||||||
|
expect(text).toBeDefined();
|
||||||
|
expect(text?.format).toEqual({
|
||||||
|
name: "codex_output_schema",
|
||||||
|
type: "json_schema",
|
||||||
|
strict: true,
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function drainEvents(events: AsyncGenerator<ThreadEvent>): Promise<void> {
|
async function drainEvents(events: AsyncGenerator<ThreadEvent>): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user