Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
```
9 changes: 5 additions & 4 deletions sdk/typescript/src/codex.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
}

/**
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion sdk/typescript/src/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
26 changes: 16 additions & 10 deletions sdk/typescript/src/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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<StreamedTurn> {
return { events: this.runStreamedInternal(input, options) };
async runStreamed(input: string): Promise<StreamedTurn> {
return { events: this.runStreamedInternal(input) };
}

private async *runStreamedInternal(
input: string,
options?: TurnOptions,
): AsyncGenerator<ThreadEvent> {
private async *runStreamedInternal(input: string): AsyncGenerator<ThreadEvent> {
const options = this._threadOptions;
const generator = this._exec.run({
input,
baseUrl: this._options.baseUrl,
Expand All @@ -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<Turn> {
const generator = this.runStreamedInternal(input, options);
async run(input: string): Promise<Turn> {
const generator = this.runStreamedInternal(input);
const items: ThreadItem[] = [];
let finalResponse: string = "";
for await (const event of generator) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion sdk/typescript/tests/codexExecSpy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ jest.mock("node:child_process", () => {
return { ...actual, spawn: jest.fn(actual.spawn) };
});

const actualChildProcess = jest.requireActual<typeof import("node:child_process")>("node:child_process");
const actualChildProcess =
jest.requireActual<typeof import("node:child_process")>("node:child_process");
const spawnMock = child_process.spawn as jest.MockedFunction<typeof actualChildProcess.spawn>;

export function codexExecSpy(): { args: string[][]; restore: () => void } {
Expand Down
26 changes: 12 additions & 14 deletions sdk/typescript/tests/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,15 @@ 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);
const secondRequest = requests[1];
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",
);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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();
}
Expand Down
Loading