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๊ฐ์ ๋ํ ์์ด์ ํธ์ ์ ์ฒด ์ฒ๋ฆฌ ์ฌ์ดํด
- Thread = ๋ํ ์ธ์
๋จ์.
ThreadEvent ์ ์ฒด ๋ชฉ๋ก
| Event type | Payload | ์ค๋ช |
|---|---|---|
thread.started | thread_id: string | Thread ์์ฑ. ์ฒซ ์ด๋ฒคํธ๋ก 1ํ๋ง ๋ฐ์ |
turn.started | (์์) | ์์ด์ ํธ ์ฒ๋ฆฌ ์์ (1 turn = ์ฌ์ฉ์ ๋ฉ์์ง 1๊ฐ์ ๋ํ ์ ์ฒด ์ฒ๋ฆฌ) |
turn.completed | usage: Usage | ์์ด์ ํธ ์ฒ๋ฆฌ ์๋ฃ. ํ ํฐ ์ฌ์ฉ๋ ํฌํจ |
turn.failed | error: { message } | ์์ด์ ํธ ์ฒ๋ฆฌ ์คํจ |
item.started | item: ThreadItem | ์ Item ์์ฑ (๋ณดํต in_progress ์ํ) |
item.updated | item: ThreadItem | Item ๋ด์ฉ ์ ๋ฐ์ดํธ (์ค๊ฐ ์ํ) |
item.completed | item: ThreadItem | Item ์๋ฃ (์ต์ข ์ํ) |
error | message: 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_message | text: string | ์์ด์ ํธ์ ํ ์คํธ ์๋ต |
reasoning | text: string | ๋ด๋ถ ์ถ๋ก ๊ณผ์ (thinking) |
command_execution | command, aggregated_output, exit_code?, status | ์ ธ ๋ช ๋ น ์คํ |
file_change | changes: FileUpdateChange[], status | ํ์ผ ๋ณ๊ฒฝ (add/delete/update) |
mcp_tool_call | server, tool, arguments, result?, error?, status | MCP ๋๊ตฌ ํธ์ถ |
web_search | query: string | ์น ๊ฒ์ |
todo_list | items: TodoItem[] | ํ ์ผ ๋ชฉ๋ก ๊ด๋ฆฌ |
error | message: 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์ ๋ฏธํ
" ์ ๋ณด๋ธ ๊ฒฝ์ฐ:
| # | Event | Item type | ํต์ฌ ๋ฐ์ดํฐ |
|---|---|---|---|
| 1 | thread.started | - | thread_id: "th_abc" |
| 2 | turn.started | - | (์์) |
| 3 | item.started | reasoning | text: "" |
| 4 | item.updated | reasoning | text: "์ฌ์ฉ์๊ฐ ํ ์ผ ๋ฑ๋ก์ ์ํจ" |
| 5 | item.completed | reasoning | text: "์ฌ์ฉ์๊ฐ ํ ์ผ ๋ฑ๋ก์ ์ํจ. createTodo ๋๊ตฌ ์ฌ์ฉ" |
| 6 | item.started | agent_message | text: "" |
| 7 | item.updated | agent_message | text: "ํ ์ผ์ ๋ฑ๋ก" |
| 8 | item.completed | agent_message | text: "ํ ์ผ์ ๋ฑ๋กํ๊ฒ ์ต๋๋ค." |
| 9 | item.started | mcp_tool_call | tool: "createTodo", status: "in_progress" |
| 10 | item.completed | mcp_tool_call | result: {...}, status: "completed" |
| 11 | item.started | agent_message | text: "" |
| 12 | item.updated | agent_message | text: "์คํ 11์ ๋ฏธํ
ํ ์ผ์ด" |
| 13 | item.completed | agent_message | text: "์คํ 11์ ๋ฏธํ
ํ ์ผ์ด ๋ฑ๋ก๋์์ต๋๋ค." |
| 14 | turn.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 ๊ณ์ฐ).