@@ -1,10 +1,9 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { SandboxMode } from "./threadOptions";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export type CodexExecArgs = {
|
||||
input: string;
|
||||
@@ -20,6 +19,8 @@ export type CodexExecArgs = {
|
||||
workingDirectory?: string;
|
||||
// --skip-git-repo-check
|
||||
skipGitRepoCheck?: boolean;
|
||||
// --output-schema
|
||||
outputSchemaFile?: string;
|
||||
};
|
||||
|
||||
export class CodexExec {
|
||||
@@ -47,6 +48,10 @@ export class CodexExec {
|
||||
commandArgs.push("--skip-git-repo-check");
|
||||
}
|
||||
|
||||
if (args.outputSchemaFile) {
|
||||
commandArgs.push("--output-schema", args.outputSchemaFile);
|
||||
}
|
||||
|
||||
if (args.threadId) {
|
||||
commandArgs.push("resume", args.threadId);
|
||||
}
|
||||
|
||||
@@ -31,3 +31,4 @@ export { Codex } from "./codex";
|
||||
export type { CodexOptions } from "./codexOptions";
|
||||
|
||||
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 { ThreadItem } from "./items";
|
||||
import { ThreadOptions } from "./threadOptions";
|
||||
import { TurnOptions } from "./turnOptions";
|
||||
import { createOutputSchemaFile } from "./outputSchemaFile";
|
||||
|
||||
/** Completed 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. */
|
||||
async runStreamed(input: string): Promise<StreamedTurn> {
|
||||
return { events: this.runStreamedInternal(input) };
|
||||
async runStreamed(input: string, turnOptions: TurnOptions = {}): Promise<StreamedTurn> {
|
||||
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 generator = this._exec.run({
|
||||
input,
|
||||
@@ -66,24 +72,29 @@ export class Thread {
|
||||
sandboxMode: options?.sandboxMode,
|
||||
workingDirectory: options?.workingDirectory,
|
||||
skipGitRepoCheck: options?.skipGitRepoCheck,
|
||||
outputSchemaFile: schemaPath,
|
||||
});
|
||||
for await (const item of generator) {
|
||||
let parsed: ThreadEvent;
|
||||
try {
|
||||
parsed = JSON.parse(item) as ThreadEvent;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse item: ${item}`, { cause: error });
|
||||
try {
|
||||
for await (const item of generator) {
|
||||
let parsed: ThreadEvent;
|
||||
try {
|
||||
parsed = JSON.parse(item) as ThreadEvent;
|
||||
} 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") {
|
||||
this._id = parsed.thread_id;
|
||||
}
|
||||
yield parsed;
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides the input to the agent and returns the completed turn. */
|
||||
async run(input: string): Promise<Turn> {
|
||||
const generator = this.runStreamedInternal(input);
|
||||
async run(input: string, turnOptions: TurnOptions = {}): Promise<Turn> {
|
||||
const generator = this.runStreamedInternal(input, turnOptions);
|
||||
const items: ThreadItem[] = [];
|
||||
let finalResponse: string = "";
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user