OpenAI Codex SDK(@openai/codex-sdk)์—์„œ ์—์ด์ „ํŠธ์˜ ๋Œ€ํ™” ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” 2๊ฐ€์ง€ ๋‹จ์œ„.

  • Thread๊ฐ€ โ€œ๋Œ€ํ™” ์„ธ์…˜โ€ ์ „์ฒด๋ฅผ ๋‹ด๋‹น
  • Turn์ด โ€œ๋ฉ”์‹œ์ง€ 1๊ฐœ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ ์‚ฌ์ดํดโ€์„ ๋‹ด๋‹น
Thread โ”€โ”€ "๋Œ€ํ™” ์„ธ์…˜" (์ „์ฒด)
  โ””โ”€โ”€ Turn โ”€โ”€ "์ฒ˜๋ฆฌ ์‚ฌ์ดํด" (๋ฉ”์‹œ์ง€ 1๊ฐœ๋งˆ๋‹ค)

OpenAI Codex SDK(@openai/codex-sdk)์—์„œ ์—์ด์ „ํŠธ์˜ ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์„ ํ‘œํ˜„ํ•˜๋Š” 2๊ณ„์ธต ํƒ€์ž… ์‹œ์Šคํ…œ.

  • Event๊ฐ€ โ€œ์–ธ์ œ/์–ด๋–ค ์ƒํƒœ ๋ณ€ํ™”โ€
  • Item์ด โ€œ๋ฌด์—‡์— ๋Œ€ํ•œ ๊ฒƒ์ธ๊ฐ€โ€๋ฅผ ๋‹ด๋‹น
ThreadEvent โ”€โ”€ "์ƒํƒœ ๋ณ€ํ™” ์•Œ๋ฆผ" (์–ธ์ œ)
  โ”œโ”€โ”€ item.started / item.updated / item.completed
  โ”‚     โ””โ”€โ”€ ThreadItem โ”€โ”€ "์ฝ˜ํ…์ธ  ๋‹จ์œ„" (๋ฌด์—‡์„)
  โ””โ”€โ”€ thread.started / turn.started / turn.completed / turn.failed / error
        โ””โ”€โ”€ (ThreadItem ์—†์Œ, ๊ฐ์ž ๊ณ ์œ  payload ๋˜๋Š” ์—†์Œ)

ํ•ด๋‹น ๊ฐœ๋…์ด ํ•„์š”ํ•œ ์ด์œ 

  • Codex SDK๋Š” ์—์ด์ „ํŠธ ์‘๋‹ต์„ ๋‹จ์ผ ๋ฌธ์ž์—ด์ด ์•„๋‹Œ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์œผ๋กœ ์ „๋‹ฌํ•จ
  • ํ…์ŠคํŠธ, ์ถ”๋ก , ๋„๊ตฌ ํ˜ธ์ถœ, ํŒŒ์ผ ๋ณ€๊ฒฝ ๋“ฑ ๋‹ค์–‘ํ•œ ์‚ฐ์ถœ๋ฌผ์ด ์„ž์—ฌ ๋‚˜์˜ค๋ฏ€๋กœ, ๊ตฌ์กฐ์  ํƒ€์ž… ์‹œ์Šคํ…œ์ด ํ•„์š”
  • Event/Item 2๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•ด์•ผ ์ŠคํŠธ๋ฆผ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์†Œ๋น„ํ•˜๊ณ  ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์Œ

AS-IS (๊ตฌ์กฐ ์—†์ด ๋‹จ์ผ ๋ฌธ์ž์—ด ์‘๋‹ต)

// ๋‹จ์ˆœ API ํ˜ธ์ถœ โ€” ์‘๋‹ต์ด ์™„์„ฑ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
const response = await api.chat("ํ• ์ผ ๋“ฑ๋กํ•ด์ค˜");
console.log(response.text); // "ํ• ์ผ์„ ๋“ฑ๋กํ–ˆ์Šต๋‹ˆ๋‹ค."
// โ†’ ์ค‘๊ฐ„ ๊ณผ์ •(์ถ”๋ก , ๋„๊ตฌ ํ˜ธ์ถœ ๋“ฑ)์ด ๋ณด์ด์ง€ ์•Š์Œ

TO-BE (Event/Item ์ŠคํŠธ๋ฆฌ๋ฐ)

// Codex SDK โ€” ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์œผ๋กœ ์ค‘๊ฐ„ ๊ณผ์ • ์‹ค์‹œ๊ฐ„ ์ˆ˜์‹ 
const { events } = await thread.runStreamed("ํ• ์ผ ๋“ฑ๋กํ•ด์ค˜");
for await (const event of events) {
  // event.type: "item.started" | "item.updated" | "item.completed" | ...
  // event.item.type: "reasoning" | "agent_message" | "mcp_tool_call" | ...
}
// โ†’ ์ถ”๋ก , ๋„๊ตฌ ํ˜ธ์ถœ, ํ…์ŠคํŠธ ์ƒ์„ฑ ๊ณผ์ •์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ด€์ฐฐ ๊ฐ€๋Šฅ

Q) โ€œ1๊ฐœ์˜ Event์— N๊ฐœ์˜ Item์ด ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋‚˜?โ€

  • ์•„๋‹ˆ๋‹ค. 1 Event : 1 Item ๊ด€๊ณ„๋‹ค. item.started/item.updated/item.completed ์˜ payload๋Š” item: ThreadItem (๋‹จ์ˆ˜, ๋ฐฐ์—ด ์•„๋‹˜)
  • N๊ฐœ์˜ Item์ด ํ•„์š”ํ•˜๋ฉด N๊ฐœ์˜ Event๊ฐ€ ๋”ฐ๋กœ ๋ฐœ์ƒํ•œ๋‹ค
  • ํ•˜๋‚˜์˜ Turn ์•ˆ์—์„œ ์—ฌ๋Ÿฌ Event๊ฐ€ ์ˆœ์ฐจ์ ์œผ๋กœ ํ๋ฅด๋Š” ๊ตฌ์กฐ

Q) โ€œThread, Turn์€ Codex์— ํŠนํ™”๋œ ๊ฐœ๋…์ธ๊ฐ€? Anthropic์ด๋‚˜ Gemini์™€ ๊ณตํ†ต์ธ๊ฐ€?โ€

  • Codex SDK ๊ณ ์œ  ๋ช…๋ช…์ด๋‹ค. Anthropic Claude SDK๋Š” query() + AsyncIterable<Message>, Gemini๋Š” Chat + sendMessageStream()์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค
  • โ€œํ•œ ๋ฒˆ์˜ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์— ๋Œ€ํ•œ ์—์ด์ „ํŠธ ์ฒ˜๋ฆฌ ์‚ฌ์ดํดโ€์ด๋ผ๋Š” ๊ฐœ๋… ์ž์ฒด๋Š” ๊ณตํ†ต์ด์ง€๋งŒ, ํƒ€์ž…๋ช…๊ณผ API ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฅด๋‹ค
  • Codex์—์„œ์˜ ์ •์˜:
    • Thread = ๋Œ€ํ™” ์„ธ์…˜ ๋‹จ์œ„. codex.startThread()๋กœ ์ƒ์„ฑ, thread_id๋กœ ์‹๋ณ„
    • Turn = Thread ์•ˆ์—์„œ ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ 1๊ฐœ์— ๋Œ€ํ•œ ์—์ด์ „ํŠธ์˜ ์ „์ฒด ์ฒ˜๋ฆฌ ์‚ฌ์ดํด

ThreadEvent ์ „์ฒด ๋ชฉ๋ก

Event typePayload์„ค๋ช…
thread.startedthread_id: stringThread ์ƒ์„ฑ. ์ฒซ ์ด๋ฒคํŠธ๋กœ 1ํšŒ๋งŒ ๋ฐœ์ƒ
turn.started(์—†์Œ)์—์ด์ „ํŠธ ์ฒ˜๋ฆฌ ์‹œ์ž‘ (1 turn = ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ 1๊ฐœ์— ๋Œ€ํ•œ ์ „์ฒด ์ฒ˜๋ฆฌ)
turn.completedusage: Usage์—์ด์ „ํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ. ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ํฌํ•จ
turn.failederror: { message }์—์ด์ „ํŠธ ์ฒ˜๋ฆฌ ์‹คํŒจ
item.starteditem: ThreadItem์ƒˆ Item ์ƒ์„ฑ (๋ณดํ†ต in_progress ์ƒํƒœ)
item.updateditem: ThreadItemItem ๋‚ด์šฉ ์—…๋ฐ์ดํŠธ (์ค‘๊ฐ„ ์ƒํƒœ)
item.completeditem: ThreadItemItem ์™„๋ฃŒ (์ตœ์ข… ์ƒํƒœ)
errormessage: string์น˜๋ช…์  ์ŠคํŠธ๋ฆผ ์—๋Ÿฌ (๋ณต๊ตฌ ๋ถˆ๊ฐ€)
// events.ts โ€” Usage ํƒ€์ž…
type Usage = {
  input_tokens: number;
  cached_input_tokens: number;
  output_tokens: number;
};

ํ•ต์‹ฌ: item.started / item.updated / item.completed ์„ธ ์ด๋ฒคํŠธ ๋ชจ๋‘ ๋™์ผํ•œ ThreadItem ๊ตฌ์กฐ์ฒด๋ฅผ ๋“ค๊ณ  ์˜จ๋‹ค. ์ฐจ์ด๋Š” Item ๋‚ด๋ถ€์˜ status ํ•„๋“œ์™€ ๋‚ด์šฉ์˜ ์™„์„ฑ๋„.

ThreadItem ์ „์ฒด ๋ชฉ๋ก

Item type์ฃผ์š” ํ•„๋“œ์„ค๋ช…
agent_messagetext: string์—์ด์ „ํŠธ์˜ ํ…์ŠคํŠธ ์‘๋‹ต
reasoningtext: string๋‚ด๋ถ€ ์ถ”๋ก  ๊ณผ์ • (thinking)
command_executioncommand, aggregated_output, exit_code?, status์…ธ ๋ช…๋ น ์‹คํ–‰
file_changechanges: FileUpdateChange[], statusํŒŒ์ผ ๋ณ€๊ฒฝ (add/delete/update)
mcp_tool_callserver, tool, arguments, result?, error?, statusMCP ๋„๊ตฌ ํ˜ธ์ถœ
web_searchquery: string์›น ๊ฒ€์ƒ‰
todo_listitems: TodoItem[]ํ•  ์ผ ๋ชฉ๋ก ๊ด€๋ฆฌ
errormessage: string์—๋Ÿฌ
// items.ts โ€” ThreadItem ์œ ๋‹ˆ์˜จ
type ThreadItem =
  | AgentMessageItem    // { id, type: "agent_message", text }
  | ReasoningItem       // { id, type: "reasoning", text }
  | CommandExecutionItem // { id, type: "command_execution", command, aggregated_output, exit_code?, status }
  | FileChangeItem      // { id, type: "file_change", changes, status }
  | McpToolCallItem     // { id, type: "mcp_tool_call", server, tool, arguments, result?, error?, status }
  | WebSearchItem       // { id, type: "web_search", query }
  | TodoListItem        // { id, type: "todo_list", items }
  | ErrorItem;          // { id, type: "error", message }

๋ผ์ดํ”„์‚ฌ์ดํด

flowchart TD
    subgraph Thread
        TS[thread.started]

        subgraph Turn
            TUS[turn.started]
            subgraph Item
                IS[item.started]
                IU[item.updated]
                IC[item.completed]
                IS --> IU
                IU -->|"๋ฐ˜๋ณต"| IU
                IU --> IC
                IS -->|"updated ์—†์ด"| IC
            end
            TUS --> IS
            IC -->|"๋‹ค์Œ Item"| IS
            IC --> TUC[turn.completed]
            TUS -->|"์‹คํŒจ"| TUF[turn.failed]
        end

        TS --> TUS
        TUC -->|"๋‹ค์Œ Turn"| TUS
    end

    Thread -->|"์น˜๋ช…์  ์—๋Ÿฌ (์ŠคํŠธ๋ฆผ ์ข…๋ฃŒ)"| ERR[error]

    style Thread fill:#fff3e0,stroke:#f57c00,color:#333
    style Turn fill:#e3f2fd,stroke:#1976d2,color:#333
    style Item fill:#e8f5e9,stroke:#388e3c,color:#333
  • thread.started: Thread ์ƒ์„ฑ ์‹œ 1ํšŒ๋งŒ ๋ฐœ์ƒ
  • turn: started ํ›„ ์ •์ƒ์ด๋ฉด completed, ์‹คํŒจํ•˜๋ฉด failed. ๋‘˜ ๋‹ค ์—†์ด ๋๋‚˜๋Š” ๊ฒฝ์šฐ๋Š” error๋กœ ์ŠคํŠธ๋ฆผ ์ž์ฒด๊ฐ€ ๋Š๊ธด ๊ฒฝ์šฐ
  • item: started โ†’ updated(0~NํšŒ ๋ฐ˜๋ณต) โ†’ completed๊ฐ€ ์ •์ƒ ํ๋ฆ„. agent_message์˜ ๊ฒฝ์šฐ ํ…์ŠคํŠธ๊ฐ€ ํ† ํฐ ๋‹จ์œ„๋กœ ๋ˆ„์ ๋˜๋ฉด์„œ updated๊ฐ€ ์—ฌ๋Ÿฌ ๋ฒˆ ๋ฐœ์ƒํ•œ๋‹ค. turn.failed๋‚˜ error ๋ฐœ์ƒ ์‹œ item.completed ์—†์ด ๋๋‚  ์ˆ˜ ์žˆ์Œ
  • turn.failed: ํ•ด๋‹น Turn ์ฆ‰์‹œ ์ข…๋ฃŒ. ์ดํ›„ ๊ฐ™์€ Turn ๋‚ด ์ด๋ฒคํŠธ ์—†์Œ. Thread๋Š” ์œ ์ง€๋˜๋ฏ€๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์ƒˆ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋ฉด ์ƒˆ Turn ์‹œ์ž‘ ๊ฐ€๋Šฅ
  • error: Thread ๋ ˆ๋ฒจ์˜ ์น˜๋ช…์  ์—๋Ÿฌ. turn/item ์•ˆ์— ์†ํ•˜์ง€ ์•Š๊ณ  ๋…๋ฆฝ์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๋ฉฐ, ์ŠคํŠธ๋ฆผ์ด ์ข…๋ฃŒ๋จ

Item์˜ ์ƒ๋ช…์ฃผ๊ธฐ: started โ†’ updated โ†’ completed

ํ•˜๋‚˜์˜ Item์€ 3๋‹จ๊ณ„ ์ด๋ฒคํŠธ๋ฅผ ๊ฑฐ์นœ๋‹ค. text ํ•„๋“œ๋Š” delta๊ฐ€ ์•„๋‹ˆ๋ผ ๋งค๋ฒˆ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€์˜ ์ „์ฒด ๋ฌธ์ž์—ด์ด๋‹ค:

sequenceDiagram
    autonumber
    participant SDK as Codex SDK
    participant App as Consumer

    SDK->>App: item.started { id: "item_01", text: "" }
    Note right of App: text = ""

    SDK->>App: item.updated { id: "item_01", text: "ํ• ์ผ์„" }
    Note right of App: text = "ํ• ์ผ์„"

    SDK->>App: item.updated { id: "item_01", text: "ํ• ์ผ์„ ๋“ฑ๋ก" }
    Note right of App: text = "ํ• ์ผ์„ ๋“ฑ๋ก"<br/>("ํ• ์ผ์„" ํฌํ•จ + " ๋“ฑ๋ก" ์ถ”๊ฐ€)

    SDK->>App: item.updated { id: "item_01", text: "ํ• ์ผ์„ ๋“ฑ๋กํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค." }
    Note right of App: text = "ํ• ์ผ์„ ๋“ฑ๋กํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค."<br/>("ํ• ์ผ์„ ๋“ฑ๋ก" ํฌํ•จ + "ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค." ์ถ”๊ฐ€)

    SDK->>App: item.completed { id: "item_01", text: "ํ• ์ผ์„ ๋“ฑ๋กํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค." }
    Note right of App: text = "ํ• ์ผ์„ ๋“ฑ๋กํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค."<br/>(๋งˆ์ง€๋ง‰ updated์™€ ๋™์ผํ•œ ์ „์ฒด ํ…์ŠคํŠธ)

Q) โ€œ๋งˆ์ง€๋ง‰ updated์™€ completed์˜ text๋Š” ๋™์ผํ•œ๊ฐ€? ๋ชจ๋“  ํ…์ŠคํŠธ๋ฅผ UI์— ๋…ธ์ถœํ•˜๋ฉด ์ค‘๋ณต๋˜๋Š” ๊ฒƒ ์•„๋‹Œ๊ฐ€?โ€

  • ๋งž๋‹ค. ๋งˆ์ง€๋ง‰ item.updated์™€ item.completed์˜ text๋Š” ๋™์ผํ•˜๋‹ค. basic_streaming.ts ์ƒ˜ํ”Œ์ด item.completed์—์„œ๋งŒ text๋ฅผ ์ถœ๋ ฅํ•˜๊ณ  item.updated๋Š” ๋ฌด์‹œํ•˜๋Š” ๊ฒƒ์ด ์ด๋ฅผ ์ฆ๋ช…ํ•œ๋‹ค
  • ๋”ฐ๋ผ์„œ UI ๋ Œ๋”๋ง ์‹œ append๊ฐ€ ์•„๋‹Œ replace ํ•ด์•ผ ํ•œ๋‹ค. ๋งค๋ฒˆ ์ „์ฒด ํ…์ŠคํŠธ๊ฐ€ ์˜ค๊ธฐ ๋•Œ๋ฌธ์— appendํ•˜๋ฉด ์ค‘๋ณต์ด ์ƒ๊น€
  • ์†Œ๋น„ ์ „๋žต 2๊ฐ€์ง€:
    • Replace: ๋งค๋ฒˆ ์ „์ฒด ํ…์ŠคํŠธ๋กœ ๊ต์ฒด (SDK ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ, ๋‹จ์ˆœํ•จ)
    • Delta ๊ณ„์‚ฐ: newText.slice(previousText.length)๋กœ ์ฆ๋ถ„๋งŒ ์ถ”์ถœ (SSE ์ŠคํŠธ๋ฆฌ๋ฐ์ฒ˜๋Ÿผ delta ์ „์†ก์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ)

์‹ค์ „ ์ด๋ฒคํŠธ ํ๋ฆ„ ์˜ˆ์‹œ

์‚ฌ์šฉ์ž๊ฐ€ "ํ• ์ผ ๋“ฑ๋กํ•ด์ค˜: ์˜คํ›„ 11์‹œ ๋ฏธํŒ…" ์„ ๋ณด๋‚ธ ๊ฒฝ์šฐ:

#EventItem typeํ•ต์‹ฌ ๋ฐ์ดํ„ฐ
1thread.started-thread_id: "th_abc"
2turn.started-(์—†์Œ)
3item.startedreasoningtext: ""
4item.updatedreasoningtext: "์‚ฌ์šฉ์ž๊ฐ€ ํ• ์ผ ๋“ฑ๋ก์„ ์›ํ•จ"
5item.completedreasoningtext: "์‚ฌ์šฉ์ž๊ฐ€ ํ• ์ผ ๋“ฑ๋ก์„ ์›ํ•จ. createTodo ๋„๊ตฌ ์‚ฌ์šฉ"
6item.startedagent_messagetext: ""
7item.updatedagent_messagetext: "ํ• ์ผ์„ ๋“ฑ๋ก"
8item.completedagent_messagetext: "ํ• ์ผ์„ ๋“ฑ๋กํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค."
9item.startedmcp_tool_calltool: "createTodo", status: "in_progress"
10item.completedmcp_tool_callresult: {...}, status: "completed"
11item.startedagent_messagetext: ""
12item.updatedagent_messagetext: "์˜คํ›„ 11์‹œ ๋ฏธํŒ… ํ• ์ผ์ด"
13item.completedagent_messagetext: "์˜คํ›„ 11์‹œ ๋ฏธํŒ… ํ• ์ผ์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
14turn.completed-usage: { input: 1234, cached: 500, output: 89 }

basic_streaming.ts ์—์„œ ์ฝ๋Š” SDK ์‚ฌ์šฉ ํŒจํ„ด

// 1. Thread ์ƒ์„ฑ
const thread = codex.startThread();
 
// 2. ๋ฉ”์‹œ์ง€ ์ „์†ก + ์ŠคํŠธ๋ฆฌ๋ฐ
const { events } = await thread.runStreamed(inputText);
 
// 3. for-await๋กœ ์ด๋ฒคํŠธ ์†Œ๋น„ (AsyncIterable)
for await (const event of events) {
  switch (event.type) {
    case "item.completed":
      // Item ํƒ€์ž…๋ณ„ ๋ถ„๊ธฐ
      switch (event.item.type) {
        case "agent_message":
          console.log(event.item.text);  // ์ตœ์ข… ์ „์ฒด ํ…์ŠคํŠธ
          break;
        case "command_execution":
          console.log(event.item.command, event.item.aggregated_output);
          break;
      }
      break;
    case "item.updated":
    case "item.started":
      // ์ƒ˜ํ”Œ์—์„œ๋Š” todo_list๋งŒ ์ฒ˜๋ฆฌ
      if (event.item.type === "todo_list") { /* ... */ }
      break;
    case "turn.completed":
      console.log(event.usage);  // ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰
      break;
  }
}

์ƒ˜ํ”Œ์˜ ํŒจํ„ด: item.completed์—์„œ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์ฝ๊ณ , item.updated๋Š” todo_list์ฒ˜๋Ÿผ ์ค‘๊ฐ„ ์ƒํƒœ๊ฐ€ ์˜๋ฏธ ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ ์ฒ˜๋ฆฌ. ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ UI๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ item.updated์˜ agent_message๋„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค (์ด์ „ text์™€ ๋น„๊ตํ•˜์—ฌ delta ๊ณ„์‚ฐ).

์ฐธ๊ณ  ๋ฌธ์„œ