diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index b74fd44b..9d2ea977 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -10,7 +10,7 @@ npm install @openai/codex-sdk ## Usage -Call `startThread()` and `run()` to start a thead with Codex. +Call `startThread()` and `run()` to start a thread with Codex. ```typescript import { Codex } from "@openai/codex-sdk"; @@ -32,17 +32,21 @@ console.log(result); ### Streaming -The `await run()` method completes when a thread turn is complete and agent is prepared the final response. +The `run()` method completes when a thread turn is complete and the agent has produced the final response. -You can thread items while they are being produced by calling `await runStreamed()`. +You can stream events while they are being produced by calling `runStreamed()` and iterating the returned generator. ```typescript -const result = thread.runStreamed("Diagnose the test failure and propose a fix"); +const { events } = await thread.runStreamed("Diagnose the test failure and propose a fix"); + +for await (const event of events) { + console.log(event); +} ``` ### Resuming a thread -If you don't have the original `Thread` instance to continue the thread, you can resume a thread by calling `resumeThread()` and providing the thread. +If you don't have the original `Thread` instance to continue the thread, you can resume by calling `resumeThread()` and providing the thread identifier. ```typescript const threadId = "..."; @@ -54,7 +58,7 @@ 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. +By default, Codex will run in the current working directory. You can change the working directory by passing the `workingDirectory` option when creating a thread. ```typescript const thread = codex.startThread({ @@ -62,7 +66,7 @@ const thread = codex.startThread({ }); ``` -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. +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 when creating a thread. ```typescript const thread = codex.startThread({ diff --git a/sdk/typescript/src/codexOptions.ts b/sdk/typescript/src/codexOptions.ts index 149e6171..2d22bcf2 100644 --- a/sdk/typescript/src/codexOptions.ts +++ b/sdk/typescript/src/codexOptions.ts @@ -2,5 +2,4 @@ export type CodexOptions = { codexPathOverride?: string; baseUrl?: string; apiKey?: string; - workingDirectory?: string; }; diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 3a4f3491..738b1dc6 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -30,4 +30,4 @@ export { Codex } from "./codex"; export type { CodexOptions } from "./codexOptions"; -export type { ThreadOptions as TheadOptions, ApprovalMode, SandboxMode } from "./threadOptions"; +export type { ThreadOptions, ApprovalMode, SandboxMode } from "./threadOptions"; diff --git a/sdk/typescript/src/thread.ts b/sdk/typescript/src/thread.ts index b31c2c51..d4745d49 100644 --- a/sdk/typescript/src/thread.ts +++ b/sdk/typescript/src/thread.ts @@ -1,5 +1,5 @@ import { CodexOptions } from "./codexOptions"; -import { ThreadEvent, Usage } from "./events"; +import { ThreadEvent, ThreadError, Usage } from "./events"; import { CodexExec } from "./exec"; import { ThreadItem } from "./items"; import { ThreadOptions } from "./threadOptions"; @@ -87,6 +87,7 @@ export class Thread { const items: ThreadItem[] = []; let finalResponse: string = ""; let usage: Usage | null = null; + let turnFailure: ThreadError | null = null; for await (const event of generator) { if (event.type === "item.completed") { if (event.item.type === "agent_message") { @@ -95,8 +96,14 @@ export class Thread { items.push(event.item); } else if (event.type === "turn.completed") { usage = event.usage; + } else if (event.type === "turn.failed") { + turnFailure = event.error; + break; } } + if (turnFailure) { + throw new Error(turnFailure.message); + } return { items, finalResponse, usage }; } } diff --git a/sdk/typescript/tests/responsesProxy.ts b/sdk/typescript/tests/responsesProxy.ts index 9ea9b31c..cf5e97fb 100644 --- a/sdk/typescript/tests/responsesProxy.ts +++ b/sdk/typescript/tests/responsesProxy.ts @@ -173,16 +173,19 @@ export function assistantMessage(text: string, itemId: string = DEFAULT_MESSAGE_ }; } +export function responseFailed(errorMessage: string): SseEvent { + return { + type: "error", + error: { code: "rate_limit_exceeded", message: errorMessage }, + }; +} + export function responseCompleted( responseId: string = DEFAULT_RESPONSE_ID, usage: ResponseCompletedUsage = DEFAULT_COMPLETED_USAGE, ): SseEvent { - const inputDetails = usage.input_tokens_details - ? { ...usage.input_tokens_details } - : null; - const outputDetails = usage.output_tokens_details - ? { ...usage.output_tokens_details } - : null; + const inputDetails = usage.input_tokens_details ? { ...usage.input_tokens_details } : null; + const outputDetails = usage.output_tokens_details ? { ...usage.output_tokens_details } : null; return { type: "response.completed", response: { diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 6b577a0b..d404761d 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -12,6 +12,7 @@ import { responseCompleted, responseStarted, sse, + responseFailed, startResponsesTestProxy, } from "./responsesProxy"; @@ -287,6 +288,23 @@ describe("Codex", () => { await close(); } }); + it("throws ThreadRunError on turn failures", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse(responseStarted("response_1")), + sse(responseFailed("rate limit exceeded")), + ], + }); + + try { + const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" }); + const thread = client.startThread(); + await expect(thread.run("fail")).rejects.toThrow("stream disconnected before completion:"); + } finally { + await close(); + } + }, 10000); // TODO(pakrym): remove timeout }); function expectPair(args: string[] | undefined, pair: [string, string]) { if (!args) {