DevOps
Tutorial Codex SDK: Otomatisasi Coding dengan TypeScript
Oktober 202611 menit baca

Codex SDK adalah library untuk menjalankan agent Codex dari kode Anda sendiri. Package TypeScript @openai/codex-sdk membungkus codex CLI: ia men-spawn codex exec dan bertukar event JSONL lewat stdin dan stdout. Agent-nya sama dengan CLI, tetapi digerakkan oleh script, sehingga cocok untuk job CI dan tool internal.
Untuk TypeScript jalankan npm install @openai/codex-sdk, yang membutuhkan Node.js 18 atau lebih baru dan hanya berjalan di server-side. Untuk Python jalankan pip install openai-codex, yang membutuhkan Python 3.10 atau lebih baru. Keduanya memakai ulang autentikasi Codex yang sudah ada, dan client TypeScript juga menerima opsi apiKey yang diteruskan ke CLI sebagai CODEX_API_KEY.
Baca thread.id setelah turn pertama dimulai, atau ambil thread_id dari event thread.started saat streaming, lalu simpan. Nanti panggil codex.resumeThread(threadId) dan lanjutkan dengan run(). Thread disimpan di disk lokal dalam folder sessions Codex, jadi resume harus dilakukan di mesin yang memiliki folder tersebut.
Pakai read-only untuk triage, review, dan penjelasan, dan workspace-write ketika agent perlu menyusun fix di dalam checkout. Hindari danger-full-access di runner CI karena runner menyimpan token dan deploy key. Set approvalPolicy ke never untuk run tanpa pengawasan, karena script tidak bisa menjawab prompt approval.
Jika Anda mem-pipe npm test ke tee tanpa mendeklarasikan shell, GitHub Actions menjalankan bash -e tanpa pipefail, sehingga step melaporkan exit code dari tee, yaitu nol. Tambahkan shell: bash pada step tersebut, yang menjalankan bash dengan -eo pipefail. Setelah itu if: failure() terpicu dan step triage Codex benar-benar berjalan.

Ringkasan Utama
Codex SDK (@openai/codex-sdk untuk TypeScript, openai-codex untuk Python) menjalankan agent Codex dari kode Anda sendiri dengan men-spawn Codex CLI. Mulai thread, panggil run() atau runStreamed(), set sandboxMode dan approvalPolicy secara eksplisit, lalu minta JSON lewat outputSchema agar job CI bisa bertindak atas hasilnya tanpa mem-parsing prosa.
Pertama kali saya ingin Codex berjalan di dalam pipeline, bukan di terminal, pertanyaannya sangat konkret: sebuah pull request baru saja merah, belum ada yang melihatnya, dan log-nya empat ratus baris output instalasi dengan satu stack trace di paling bawah. Bisakah sebuah agent membaca log itu, membuka kodenya, menjelaskan apa yang rusak, lalu berhenti di situ kecuali saya minta lebih? Codex SDK adalah bagian yang membuat hal itu menjadi script, bukan sesi chat.
Tutorial Codex SDK ini membangun job tersebut dengan TypeScript: thread pertama, pengaturan sandbox dan approval yang penting di CI, streaming event, hasil triage terstruktur, workflow GitHub Actions, dan thread yang dilanjutkan untuk menyusun fix. Setiap nama opsi di bawah diambil dari type definition SDK di repository openai/codex dan dari dokumentasi SDK OpenAI, bukan dari ingatan. Ini panduan membangun; untuk memilih antara Codex dan Claude Code sebagai alat harian, post perbandingan di situs ini sudah membahasnya.
SDK ini bukan HTTP API baru. Package TypeScript-nya membungkus codex CLI dari @openai/codex: ia men-spawn codex exec dengan output JSON eksperimental dan bertukar event JSONL lewat stdin dan stdout. Satu keputusan desain itu menjelaskan sebagian besar perilakunya. SDK hanya untuk server-side, butuh filesystem dan working directory, mewarisi konfigurasi CLI, dan thread tersimpan di disk lokal dalam folder sessions di bawah direktori home Codex.
| Aspek | TypeScript | Python |
|---|---|---|
| Package dan instalasi | npm install @openai/codex-sdk | pip install openai-codex |
| Runtime | Node.js 18 atau lebih baru | Python 3.10 atau lebih baru, dengan runtime Codex CLI yang di-pin |
| Memulai dan melanjutkan | startThread(), lalu run() berulang kali; resumeThread(id) | thread_start(), lalu run() berulang kali |
| Gaya async | Promise, ditambah runStreamed() sebagai async generator | Client Codex sinkron, atau AsyncCodex dengan async with |
| Pengaturan sandbox | String sandboxMode di thread options | Enum Sandbox, bisa di-override per run() |
Karena SDK menggerakkan CLI, agent benar-benar bekerja di mesin tempat ia berjalan: menjalankan perintah shell, menerapkan patch, dan membaca file di working directory. Justru itu tujuannya, dan itu juga alasan bagian sandbox di bawah wajib dibaca.
Cek versi sebelum Anda pin: saat tulisan ini dibuat, npm registry mencantumkan @openai/codex-sdk 0.159.3, yang bergantung pada @openai/codex dengan versi yang persis sama. Upgrade keduanya bersamaan dengan meng-upgrade SDK, jangan pernah CLI-nya saja, atau bentuk event JSONL yang di-parse SDK bisa bergeser dari yang dikeluarkan CLI.
Thread adalah satu percakapan dengan agent. Setiap panggilan run() adalah satu turn: agent membuat rencana, menjalankan perintah, mengedit file bila diizinkan, lalu selesai dengan pesan akhir. Turn yang dikembalikan membawa finalResponse, daftar lengkap item, dan pemakaian token, jadi Anda mendapat jawaban sekaligus jejak audit dari satu panggilan.
npm install @openai/codex-sdk # pulls the matching @openai/codex CLI as a dependency
// first-thread.ts — Node 18+, server-side only
import { Codex } from "@openai/codex-sdk";
// The SDK spawns `codex exec --experimental-json` and reads JSONL from stdout.
// apiKey is forwarded to the CLI as CODEX_API_KEY.
const codex = new Codex({ apiKey: process.env.CODEX_API_KEY });
const thread = codex.startThread({
workingDirectory: process.cwd(), // must be a Git repo unless skipGitRepoCheck: true
sandboxMode: "read-only",
approvalPolicy: "never", // a script cannot answer an approval prompt
});
const turn = await thread.run("Explain how the test suite in this repo is organised.");
console.log(turn.finalResponse); // the agent's last message
console.log(turn.items.length); // every command, file change and message in the turn
console.log(turn.usage); // input, cached, output and reasoning tokens
// thread.id is null until the first turn has started — read it after run().
console.log("resume later with", thread.id);Dua detail di sini membuang waktu kalau dilewatkan. Working directory harus berupa repository Git kecuali Anda mengirim skipGitRepoCheck, sebuah pengaman yang disengaja agar edit dari agent selalu bisa dipulihkan dengan git; di CI hasil checkout sudah memenuhinya. Selain itu thread.id tetap null sampai turn pertama dimulai, jadi kode yang menyimpan id sebelum memanggil run() sebenarnya tidak menyimpan apa-apa.
Sandbox menentukan apa yang boleh disentuh perintah agent; approval policy menentukan kapan ia berhenti untuk bertanya kepada manusia. Di terminal, default-nya disetel untuk orang yang sedang mengawasi. Di CI tidak ada yang mengawasi, jadi set keduanya secara eksplisit di setiap thread alih-alih mewarisi apa pun yang tertulis di konfigurasi Codex milik runner.
| sandboxMode TypeScript | Preset Python | Di mana saya memakainya |
|---|---|---|
| read-only | Sandbox.read_only | Triage, review, dan penjelasan. Agent bisa membaca dan menjalankan perintah read-only tetapi tidak bisa mengubah checkout. |
| workspace-write | Sandbox.workspace_write | Menyusun fix atau migrasi. Penulisan tetap di dalam working directory; akses network adalah switch terpisah. |
| danger-full-access | Sandbox.full_access | Hampir tidak pernah di runner CI, yang menyimpan deploy key dan token. Hanya di dalam container sekali pakai milik Anda sendiri. |
Approval policy menerima never, on-request, on-failure, dan untrusted. Script tidak bisa menekan tombol approve, jadi thread tanpa pengawasan memakai never dan mengandalkan sandbox untuk keamanan. Akses network punya flag sendiri, networkAccessEnabled, dan saya mematikannya untuk triage: mendiagnosis kegagalan dari kode dan log seharusnya tidak butuh internet, dan baris log yang berisi prompt injection juga tidak boleh bisa menjangkaunya.
run() menahan semuanya sampai turn selesai. Untuk task CI sepuluh menit, artinya sepuluh menit tanpa output di log job. runStreamed() justru mengembalikan async generator berisi event bertipe, jadi log job menampilkan setiap perintah saat agent menjalankannya.
const { events } = await thread.runStreamed("Fix the failing test in src/invoice.test.ts");
for await (const event of events) {
switch (event.type) {
case "thread.started":
// The earliest moment the id exists. Persist it before anything can crash.
await saveThreadId(event.thread_id);
break;
case "item.completed":
if (event.item.type === "command_execution") {
console.log(`$ ${event.item.command} -> exit ${event.item.exit_code}`);
}
if (event.item.type === "file_change") {
for (const change of event.item.changes) console.log(`${change.kind} ${change.path}`);
}
break;
case "turn.completed":
console.log("tokens in/out", event.usage.input_tokens, event.usage.output_tokens);
break;
case "turn.failed":
// run() throws on this; runStreamed() hands it to you, so you must throw yourself.
throw new Error(event.error.message);
}
}Poin terakhir itulah yang paling ingin saya garis bawahi. Loop streaming yang hanya menangani item.completed dan turn.completed akan keluar dengan bersih saat turn gagal, dan step CI menjadi hijau tanpa hasil apa pun yang ditulis. Perlakukan turn.failed sebagai exception, setiap kali.
Paragraf triage memang enak dibaca tetapi tidak berguna bagi step workflow berikutnya. Mengirim outputSchema pada sebuah turn membuat pesan akhir berupa string JSON yang sesuai schema, sehingga job bisa bercabang berdasarkan field verdict. Schema di bawah memaksa satu dari empat verdict, karena hal paling berguna yang bisa disampaikan triage adalah apakah kode perlu disentuh sama sekali.
// scripts/codex-triage.ts — runs only after the test step has failed
import { readFileSync, writeFileSync } from "node:fs";
import { Codex } from "@openai/codex-sdk";
const TRIAGE_SCHEMA = {
type: "object",
properties: {
verdict: { type: "string", enum: ["product_bug", "test_bug", "flaky", "environment"] },
failingTests: { type: "array", items: { type: "string" } },
rootCause: { type: "string" },
suggestedFix: { type: "string" },
},
required: ["verdict", "failingTests", "rootCause", "suggestedFix"],
additionalProperties: false,
} as const;
const TEN_MINUTES_MS = 10 * 60_000;
const LOG_TAIL_CHARS = 20_000; // the tail holds the stack traces; the head is install noise
const codex = new Codex({ apiKey: process.env.CODEX_API_KEY });
const thread = codex.startThread({
sandboxMode: "read-only", // triage reads; it never writes
approvalPolicy: "never",
networkAccessEnabled: false,
});
const logTail = readFileSync("test-output.log", "utf8").slice(-LOG_TAIL_CHARS);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TEN_MINUTES_MS);
try {
const turn = await thread.run(
"The test suite failed. Find the root cause without modifying any file.\n\n" + logTail,
{ outputSchema: TRIAGE_SCHEMA, signal: controller.signal },
);
// With outputSchema set, finalResponse is a JSON string that matches the schema.
const triage = JSON.parse(turn.finalResponse);
writeFileSync(
"triage.json",
JSON.stringify({ threadId: thread.id, usage: turn.usage, ...triage }, null, 2),
);
} finally {
clearTimeout(timer);
}AbortSignal di TurnOptions adalah batas biayanya. Tanpa itu, turn yang macet berjalan sampai timeout job itu sendiri dan terus menghabiskan token; dengan itu, script yang menentukan anggarannya. Saya juga hanya mengirim bagian akhir log, karena stack trace ada di ujung, sementara output instalasi dependency di awal hanya menambah token tanpa menambah bukti.
Workflow menjalankan test, dan hanya jika gagal ia menjalankan script triage lalu meng-upload triage.json sebagai artifact. Step yang paling penting justru bukan step Codex, melainkan deklarasi shell di step test, karena cara paling jelas untuk menyimpan log diam-diam mematahkan kondisi kegagalannya.
# .github/workflows/test.yml
name: test
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
# Wrong: the default shell is `bash -e` WITHOUT pipefail, so tee's exit code
# (0) wins and a failing suite turns the step green.
# - run: npm test 2>&1 | tee test-output.log
# Right: an explicit `shell: bash` runs with -eo pipefail.
- run: npm test 2>&1 | tee test-output.log
shell: bash
- name: Triage with Codex
if: failure()
run: npx tsx scripts/codex-triage.ts
env:
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: codex-triage
path: triage.jsonJebakan pipe: GitHub Actions menjalankan shell yang tidak dideklarasikan sebagai bash -e tanpa pipefail, jadi npm test yang di-pipe ke tee melaporkan exit code dari tee, yaitu nol. Test suite gagal, step lolos, if: failure() tidak pernah terpicu, dan agent tidak pernah berjalan. Mendeklarasikan shell: bash mengubah step menjadi bash dengan -eo pipefail sehingga kegagalannya ikut diteruskan.
Menyusun fix adalah turn kedua di thread yang sama, bukan percakapan baru. resumeThread menerima thread options yang baru, jadi thread triage yang tadinya read-only bisa dilanjutkan dengan workspace-write dan sudah tahu apa yang ia temukan. Session tersimpan di disk runner, jadi ini berjalan di dalam satu job; lintas job Anda perlu membawa direktori sessions Codex.
// scripts/codex-draft-fix.ts — same job, after a human-readable triage exists
import { readFileSync } from "node:fs";
import { Codex } from "@openai/codex-sdk";
const { threadId, verdict } = JSON.parse(readFileSync("triage.json", "utf8"));
if (verdict !== "product_bug" && verdict !== "test_bug") process.exit(0); // never "fix" a flake
const codex = new Codex({ apiKey: process.env.CODEX_API_KEY });
// Same conversation, wider permissions: resumeThread takes fresh ThreadOptions.
const thread = codex.resumeThread(threadId, {
sandboxMode: "workspace-write",
approvalPolicy: "never",
});
await thread.run(
"Apply the fix you proposed. Keep the diff minimal, then run the failing tests again.",
);
// The workflow, not the agent, commits and opens the PR: git switch -c, git commit, gh pr create.SDK Python mencakup hal yang sama dengan penamaan ala Python: Codex sebagai context manager, thread_start() alih-alih startThread(), final_response alih-alih finalResponse, dan client AsyncCodex untuk kode asyncio. Perbedaan paling bergunanya adalah sandbox bisa di-override pada satu panggilan run(), tidak hanya saat thread dimulai.
# pip install openai-codex (Python 3.10+)
from openai_codex import Codex, Sandbox
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
thread.run("Migrate the date helpers in src/utils from moment to date-fns.")
# Same thread, narrower sandbox for this one run: the reviewer cannot edit
# the code it is reviewing.
review = thread.run("Review the diff only. List anything risky.", sandbox=Sandbox.read_only)
print(review.final_response)Override per run itu menghasilkan loop tulis-lalu-review yang rapi: thread yang sama yang membuat perubahan me-review diff-nya sendiri di bawah read_only, sehingga tahap review secara fisik tidak bisa mengedit hal yang sedang ia nilai. Di TypeScript padanannya adalah melanjutkan thread dengan options berbeda, seperti di contoh CI. README Python juga mendokumentasikan helper login eksplisit, termasuk login dengan API key dan device code, yang penting di server headless.
Di DevDay pada 29 September 2026, OpenAI memberi Codex cloud environment yang bisa dipakai ulang dan dibagikan lintas perangkat dengan pengaturan dan izin yang sudah disetujui, CLI dengan kontrol suara dan tampilan /agents baru untuk mendelegasikan serta memantau beberapa task, perbaikan pada session resume dan worktree, serta pengalaman code review baru di aplikasi desktop ChatGPT yang bisa melakukan review otomatis saat Anda sedang offline.
Tidak satu pun dari itu menggantikan SDK untuk pekerjaan pipeline. SDK tetap menggerakkan agent lokal di direktori yang Anda kendalikan, dan itulah persisnya sebuah runner CI. Cloud environment dan code review yang di-hosting lebih cocok ketika pekerjaan itu sama sekali tidak boleh berjalan di infrastruktur Anda, dan tampilan /agents ditujukan bagi orang yang sedang mengatur banyak task di terminal. Pilih berdasarkan di mana kode seharusnya dieksekusi, bukan berdasarkan fitur mana yang paling baru.
Mulai setiap otomatisasi baru dalam mode read-only dengan outputSchema, dan biarkan begitu selama seminggu. Membaca verdict-nya memberi tahu Anda apakah agent memahami repository Anda sebelum ia diizinkan menulis satu baris pun, dan minggu itu tidak memerlukan rollback apa pun.
Aturan yang saya bawa dari sini: agent tanpa pengawasan adalah step CI seperti yang lain, jadi beri disiplin yang sama. Set sandbox dan approval policy di setiap thread, minta JSON alih-alih prosa, beri batas pada setiap turn dengan AbortSignal, perlakukan turn.failed sebagai exception, dan biarkan commit serta push tetap di workflow tempat reviewer bisa melihatnya. Mulai dari read-only, lalu dapatkan akses tulis secara bertahap.
Sumber