Store settings on the thread instead of turn (#4579)
It's much more common to keep the same settings for the entire conversation, we can add per-turn overrides later.
This commit is contained in:
@@ -51,3 +51,21 @@ const result = await thread.run("Implement the fix");
|
|||||||
|
|
||||||
console.log(result);
|
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,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CodexOptions } from "./codexOptions";
|
import { CodexOptions } from "./codexOptions";
|
||||||
import { CodexExec } from "./exec";
|
import { CodexExec } from "./exec";
|
||||||
import { Thread } from "./thread";
|
import { Thread } from "./thread";
|
||||||
|
import { ThreadOptions } from "./threadOptions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex is the main class for interacting with the Codex agent.
|
* 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.
|
* Starts a new conversation with an agent.
|
||||||
* @returns A new thread instance.
|
* @returns A new thread instance.
|
||||||
*/
|
*/
|
||||||
startThread(): Thread {
|
startThread(options: ThreadOptions = {}): Thread {
|
||||||
return new Thread(this.exec, this.options);
|
return new Thread(this.exec, this.options, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,7 +32,7 @@ export class Codex {
|
|||||||
* @param id The id of the thread to resume.
|
* @param id The id of the thread to resume.
|
||||||
* @returns A new thread instance.
|
* @returns A new thread instance.
|
||||||
*/
|
*/
|
||||||
resumeThread(id: string): Thread {
|
resumeThread(id: string, options: ThreadOptions = {}): Thread {
|
||||||
return new Thread(this.exec, this.options, id);
|
return new Thread(this.exec, this.options, options, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|||||||
|
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
|
|
||||||
import { SandboxMode } from "./turnOptions";
|
import { SandboxMode } from "./threadOptions";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ export { Codex } from "./codex";
|
|||||||
|
|
||||||
export type { CodexOptions } from "./codexOptions";
|
export type { CodexOptions } from "./codexOptions";
|
||||||
|
|
||||||
export type { TurnOptions, ApprovalMode, SandboxMode } from "./turnOptions";
|
export type { ThreadOptions as TheadOptions, ApprovalMode, SandboxMode } from "./threadOptions";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CodexOptions } from "./codexOptions";
|
|||||||
import { ThreadEvent } from "./events";
|
import { ThreadEvent } from "./events";
|
||||||
import { CodexExec } from "./exec";
|
import { CodexExec } from "./exec";
|
||||||
import { ThreadItem } from "./items";
|
import { ThreadItem } from "./items";
|
||||||
import { TurnOptions } from "./turnOptions";
|
import { ThreadOptions } from "./threadOptions";
|
||||||
|
|
||||||
/** Completed turn. */
|
/** Completed turn. */
|
||||||
export type Turn = {
|
export type Turn = {
|
||||||
@@ -29,27 +29,33 @@ export class Thread {
|
|||||||
private _exec: CodexExec;
|
private _exec: CodexExec;
|
||||||
private _options: CodexOptions;
|
private _options: CodexOptions;
|
||||||
private _id: string | null;
|
private _id: string | null;
|
||||||
|
private _threadOptions: ThreadOptions;
|
||||||
|
|
||||||
/** Returns the ID of the thread. Populated after the first turn starts. */
|
/** Returns the ID of the thread. Populated after the first turn starts. */
|
||||||
public get id(): string | null {
|
public get id(): string | null {
|
||||||
return this._id;
|
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._exec = exec;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
this._id = id;
|
this._id = id;
|
||||||
|
this._threadOptions = threadOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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, options?: TurnOptions): Promise<StreamedTurn> {
|
async runStreamed(input: string): Promise<StreamedTurn> {
|
||||||
return { events: this.runStreamedInternal(input, options) };
|
return { events: this.runStreamedInternal(input) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async *runStreamedInternal(
|
private async *runStreamedInternal(input: string): AsyncGenerator<ThreadEvent> {
|
||||||
input: string,
|
const options = this._threadOptions;
|
||||||
options?: TurnOptions,
|
|
||||||
): AsyncGenerator<ThreadEvent> {
|
|
||||||
const generator = this._exec.run({
|
const generator = this._exec.run({
|
||||||
input,
|
input,
|
||||||
baseUrl: this._options.baseUrl,
|
baseUrl: this._options.baseUrl,
|
||||||
@@ -75,8 +81,8 @@ export class Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 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, options?: TurnOptions): Promise<Turn> {
|
async run(input: string): Promise<Turn> {
|
||||||
const generator = this.runStreamedInternal(input, options);
|
const generator = this.runStreamedInternal(input);
|
||||||
const items: ThreadItem[] = [];
|
const items: ThreadItem[] = [];
|
||||||
let finalResponse: string = "";
|
let finalResponse: string = "";
|
||||||
for await (const event of generator) {
|
for await (const event of generator) {
|
||||||
|
|||||||
@@ -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 SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
||||||
|
|
||||||
export type TurnOptions = {
|
export type ThreadOptions = {
|
||||||
model?: string;
|
model?: string;
|
||||||
sandboxMode?: SandboxMode;
|
sandboxMode?: SandboxMode;
|
||||||
workingDirectory?: string;
|
workingDirectory?: string;
|
||||||
@@ -5,7 +5,8 @@ jest.mock("node:child_process", () => {
|
|||||||
return { ...actual, spawn: jest.fn(actual.spawn) };
|
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>;
|
const spawnMock = child_process.spawn as jest.MockedFunction<typeof actualChildProcess.spawn>;
|
||||||
|
|
||||||
export function codexExecSpy(): { args: string[][]; restore: () => void } {
|
export function codexExecSpy(): { args: string[][]; restore: () => void } {
|
||||||
|
|||||||
@@ -109,9 +109,7 @@ describe("Codex", () => {
|
|||||||
|
|
||||||
const thread = client.startThread();
|
const thread = client.startThread();
|
||||||
await thread.run("first input");
|
await thread.run("first input");
|
||||||
await thread.run("second input", {
|
await thread.run("second input");
|
||||||
model: "gpt-test-1",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check second request continues the same thread
|
// Check second request continues the same thread
|
||||||
expect(requests.length).toBeGreaterThanOrEqual(2);
|
expect(requests.length).toBeGreaterThanOrEqual(2);
|
||||||
@@ -119,7 +117,7 @@ describe("Codex", () => {
|
|||||||
expect(secondRequest).toBeDefined();
|
expect(secondRequest).toBeDefined();
|
||||||
const payload = secondRequest!.json;
|
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(
|
const assistantEntry = payload.input.find(
|
||||||
(entry: { role: string }) => entry.role === "assistant",
|
(entry: { role: string }) => entry.role === "assistant",
|
||||||
);
|
);
|
||||||
@@ -197,11 +195,11 @@ describe("Codex", () => {
|
|||||||
try {
|
try {
|
||||||
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
|
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
|
||||||
|
|
||||||
const thread = client.startThread();
|
const thread = client.startThread({
|
||||||
await thread.run("apply options", {
|
|
||||||
model: "gpt-test-1",
|
model: "gpt-test-1",
|
||||||
sandboxMode: "workspace-write",
|
sandboxMode: "workspace-write",
|
||||||
});
|
});
|
||||||
|
await thread.run("apply options");
|
||||||
|
|
||||||
const payload = requests[0];
|
const payload = requests[0];
|
||||||
expect(payload).toBeDefined();
|
expect(payload).toBeDefined();
|
||||||
@@ -240,11 +238,11 @@ describe("Codex", () => {
|
|||||||
apiKey: "test",
|
apiKey: "test",
|
||||||
});
|
});
|
||||||
|
|
||||||
const thread = client.startThread();
|
const thread = client.startThread({
|
||||||
await thread.run("use custom working directory", {
|
|
||||||
workingDirectory,
|
workingDirectory,
|
||||||
skipGitRepoCheck: true,
|
skipGitRepoCheck: true,
|
||||||
});
|
});
|
||||||
|
await thread.run("use custom working directory");
|
||||||
|
|
||||||
const commandArgs = spawnArgs[0];
|
const commandArgs = spawnArgs[0];
|
||||||
expectPair(commandArgs, ["--cd", workingDirectory]);
|
expectPair(commandArgs, ["--cd", workingDirectory]);
|
||||||
@@ -274,12 +272,12 @@ describe("Codex", () => {
|
|||||||
apiKey: "test",
|
apiKey: "test",
|
||||||
});
|
});
|
||||||
|
|
||||||
const thread = client.startThread();
|
const thread = client.startThread({
|
||||||
await expect(
|
workingDirectory,
|
||||||
thread.run("use custom working directory", {
|
});
|
||||||
workingDirectory,
|
await expect(thread.run("use custom working directory")).rejects.toThrow(
|
||||||
}),
|
/Not inside a trusted directory/,
|
||||||
).rejects.toThrow(/Not inside a trusted directory/);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await close();
|
await close();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user