diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 19accb0b30..b74fd44ba3 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -51,3 +51,21 @@ const result = await thread.run("Implement the fix"); console.log(result); ``` + +### Working directory + +By default, Codex will run in the current working directory. You can change the working directory by passing the `workingDirectory` option to the when creating a thread. + +```typescript +const thread = codex.startThread({ + workingDirectory: "/path/to/working/directory", +}); +``` + +To avoid unrecoverable errors, Codex requires the working directory to be a git repository. You can skip the git repository check by passing the `skipGitRepoCheck` option to the when creating a thread. + +```typescript +const thread = codex.startThread({ + skipGitRepoCheck: true, +}); +``` diff --git a/sdk/typescript/src/codex.ts b/sdk/typescript/src/codex.ts index 8f87122839..84376e677e 100644 --- a/sdk/typescript/src/codex.ts +++ b/sdk/typescript/src/codex.ts @@ -1,6 +1,7 @@ import { CodexOptions } from "./codexOptions"; import { CodexExec } from "./exec"; import { Thread } from "./thread"; +import { ThreadOptions } from "./threadOptions"; /** * Codex is the main class for interacting with the Codex agent. @@ -20,8 +21,8 @@ export class Codex { * Starts a new conversation with an agent. * @returns A new thread instance. */ - startThread(): Thread { - return new Thread(this.exec, this.options); + startThread(options: ThreadOptions = {}): Thread { + return new Thread(this.exec, this.options, options); } /** @@ -31,7 +32,7 @@ export class Codex { * @param id The id of the thread to resume. * @returns A new thread instance. */ - resumeThread(id: string): Thread { - return new Thread(this.exec, this.options, id); + resumeThread(id: string, options: ThreadOptions = {}): Thread { + return new Thread(this.exec, this.options, options, id); } } diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index cdb1982f0d..91ce763909 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -2,7 +2,7 @@ import { spawn } from "node:child_process"; import readline from "node:readline"; -import { SandboxMode } from "./turnOptions"; +import { SandboxMode } from "./threadOptions"; import path from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index f2f84d153c..b759b3c68f 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -29,4 +29,4 @@ export { Codex } from "./codex"; export type { CodexOptions } from "./codexOptions"; -export type { TurnOptions, ApprovalMode, SandboxMode } from "./turnOptions"; +export type { ThreadOptions as TheadOptions, ApprovalMode, SandboxMode } from "./threadOptions"; diff --git a/sdk/typescript/src/thread.ts b/sdk/typescript/src/thread.ts index 7ec710217c..fe32a27136 100644 --- a/sdk/typescript/src/thread.ts +++ b/sdk/typescript/src/thread.ts @@ -2,7 +2,7 @@ import { CodexOptions } from "./codexOptions"; import { ThreadEvent } from "./events"; import { CodexExec } from "./exec"; import { ThreadItem } from "./items"; -import { TurnOptions } from "./turnOptions"; +import { ThreadOptions } from "./threadOptions"; /** Completed turn. */ export type Turn = { @@ -29,27 +29,33 @@ export class Thread { private _exec: CodexExec; private _options: CodexOptions; private _id: string | null; + private _threadOptions: ThreadOptions; /** Returns the ID of the thread. Populated after the first turn starts. */ public get id(): string | null { return this._id; } - constructor(exec: CodexExec, options: CodexOptions, id: string | null = null) { + /* @internal */ + constructor( + exec: CodexExec, + options: CodexOptions, + threadOptions: ThreadOptions, + id: string | null = null, + ) { this._exec = exec; this._options = options; this._id = id; + this._threadOptions = threadOptions; } /** Provides the input to the agent and streams events as they are produced during the turn. */ - async runStreamed(input: string, options?: TurnOptions): Promise { - return { events: this.runStreamedInternal(input, options) }; + async runStreamed(input: string): Promise { + return { events: this.runStreamedInternal(input) }; } - private async *runStreamedInternal( - input: string, - options?: TurnOptions, - ): AsyncGenerator { + private async *runStreamedInternal(input: string): AsyncGenerator { + const options = this._threadOptions; const generator = this._exec.run({ input, baseUrl: this._options.baseUrl, @@ -75,8 +81,8 @@ export class Thread { } /** Provides the input to the agent and returns the completed turn. */ - async run(input: string, options?: TurnOptions): Promise { - const generator = this.runStreamedInternal(input, options); + async run(input: string): Promise { + const generator = this.runStreamedInternal(input); const items: ThreadItem[] = []; let finalResponse: string = ""; for await (const event of generator) { diff --git a/sdk/typescript/src/turnOptions.ts b/sdk/typescript/src/threadOptions.ts similarity index 90% rename from sdk/typescript/src/turnOptions.ts rename to sdk/typescript/src/threadOptions.ts index ce56d7b2c6..7f01488d23 100644 --- a/sdk/typescript/src/turnOptions.ts +++ b/sdk/typescript/src/threadOptions.ts @@ -2,7 +2,7 @@ export type ApprovalMode = "never" | "on-request" | "on-failure" | "untrusted"; export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access"; -export type TurnOptions = { +export type ThreadOptions = { model?: string; sandboxMode?: SandboxMode; workingDirectory?: string; diff --git a/sdk/typescript/tests/codexExecSpy.ts b/sdk/typescript/tests/codexExecSpy.ts index 3715f71e0b..bf7cbd6cc8 100644 --- a/sdk/typescript/tests/codexExecSpy.ts +++ b/sdk/typescript/tests/codexExecSpy.ts @@ -5,7 +5,8 @@ jest.mock("node:child_process", () => { return { ...actual, spawn: jest.fn(actual.spawn) }; }); -const actualChildProcess = jest.requireActual("node:child_process"); +const actualChildProcess = + jest.requireActual("node:child_process"); const spawnMock = child_process.spawn as jest.MockedFunction; export function codexExecSpy(): { args: string[][]; restore: () => void } { diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 2f15237f4f..357d8cd0db 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -109,9 +109,7 @@ describe("Codex", () => { const thread = client.startThread(); await thread.run("first input"); - await thread.run("second input", { - model: "gpt-test-1", - }); + await thread.run("second input"); // Check second request continues the same thread expect(requests.length).toBeGreaterThanOrEqual(2); @@ -119,7 +117,7 @@ describe("Codex", () => { expect(secondRequest).toBeDefined(); const payload = secondRequest!.json; - expect(payload.model).toBe("gpt-test-1"); + expect(payload.input.at(-1)!.content![0]!.text).toBe("second input"); const assistantEntry = payload.input.find( (entry: { role: string }) => entry.role === "assistant", ); @@ -197,11 +195,11 @@ describe("Codex", () => { try { const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" }); - const thread = client.startThread(); - await thread.run("apply options", { + const thread = client.startThread({ model: "gpt-test-1", sandboxMode: "workspace-write", }); + await thread.run("apply options"); const payload = requests[0]; expect(payload).toBeDefined(); @@ -240,11 +238,11 @@ describe("Codex", () => { apiKey: "test", }); - const thread = client.startThread(); - await thread.run("use custom working directory", { + const thread = client.startThread({ workingDirectory, skipGitRepoCheck: true, }); + await thread.run("use custom working directory"); const commandArgs = spawnArgs[0]; expectPair(commandArgs, ["--cd", workingDirectory]); @@ -274,12 +272,12 @@ describe("Codex", () => { apiKey: "test", }); - const thread = client.startThread(); - await expect( - thread.run("use custom working directory", { - workingDirectory, - }), - ).rejects.toThrow(/Not inside a trusted directory/); + const thread = client.startThread({ + workingDirectory, + }); + await expect(thread.run("use custom working directory")).rejects.toThrow( + /Not inside a trusted directory/, + ); } finally { await close(); }