上一篇介紹 Codex Core 時,看到它用 Submission / Event 跟 Client 互動。
Core 負責真正的 Agent loop:
收到任務
↓
呼叫 Model
↓
執行 Tool / Command
↓
修改檔案
↓
產生 Event
↓
繼續下一步
但這裡留下一個問題:
如果 Client 不再是 Codex 自己的 TUI,而是 Web、Slack、LINE 或 IDE,這些產品要怎麼操作同一套 Codex Runtime?
這就是 codex app-server 要解決的事情。
它是在 Codex Core 上面建立一條穩定的產品介面:
Slack / LINE / Web / IDE
│
▼
Codex App Server
│
▼
Codex Core
假設我要做一個 Slack Agent。
使用者輸入:
@codex 幫我跑 pytest,
看看是哪裡壞掉。
如果 Slack Backend 直接接 Codex Core,它就必須知道:
ThreadManager
Submission
EventMsg
Approval
Turn lifecycle
Tool execution
Agent resume
等於 Slack Adapter 同時也要變成 Codex Runtime Client。
App Server 做的事情,是在中間建立一套比較穩定的模型:
Slack Adapter
│
├─ Slack app_mention
├─ Slack button
│
└─ Codex App Server Protocol
├─ thread/start
├─ turn/start
├─ turn/completed
└─ requestApproval
所以 Slack、LINE、Web 不必理解 Core 裡到底是哪一個 Rust EventMsg。
它們只需要理解:
turn/start
item/started
item/completed
turn/completed
假設 LINE 使用者傳:
[error.png]
這是什麼問題?
LINE 本身可能只透過 Webhook 告訴我們:
message.type = image
message.id = 123456
Codex 當然不知道 LINE 的 message.id 是什麼。
所以 LINE Adapter 先轉成 App Server 定義的 UserInput:
{
"method": "turn/start",
"params": {
"threadId": "thr_001",
"input": [
{
"type": "text",
"text": "這是什麼問題?"
},
{
"type": "localImage",
"path": "/workspace/error.png"
}
]
}
}
目前 App Server 的
UserInput已定義文字、圖片、本地圖片、audio、skill、mention 等輸入型別。
所以不同平台最後可以變成:
LINE image ──┐
│
Slack image ─┼── Adapter ──► localImage
│
Web upload ──┘
到這裡,Agent 開始執行。
App Server 不會只等最後答案。它會把這些 Runtime activity 持續轉成 Item:
turn/started
item/started
type = commandExecution
item/completed
item/started
type = fileChange
item/completed
item/started
type = commandExecution
item/completed
turn/completed
目前 ThreadItem 也已經有許多明確類型,例如:
agentMessage
reasoning
commandExecution
fileChange
mcpToolCall
webSearch
imageView
...
於是 Slack 可以看到:
🔧 Running pytest...
📝 Editing foo.py...
🔧 Running pytest...
✅ Completed
再傳最終答案。
這裡可以看出 App Server 做的第一件核心工作:
把一次 Agent Turn 的輸入與執行過程,整理成 Client 可以穩定理解的 lifecycle。
外面的 Interface 可以不同,但對 Codex 來說都是:
UserInput
↓
Turn
↓
Item
↓
Turn Complete
假設 Agent 執行到一半想做:
rm old.db
但這個操作需要使用者批准。
這時就不能只是:
item/started
item/completed
因為 Agent 現在缺一個外部答案。
所以流程會從單向 Event:
Agent
↓
Client
變成雙向:
Agent
↓
Request
↓
Client
↓
Response
↓
Agent 繼續
例如 App Server 發出:
request #77
是否允許執行:
rm old.db
Slack 可以把它顯示成:
Codex 想執行:
rm old.db
[Approve] [Decline]
LINE 則可能是:
Codex 想刪除 old.db
[同意] [拒絕]
這兩個 UI 完全不同。
App Server 不需要知道:
Approve
同意
Yes
✅
各自的 Adapter 只需要轉成共同語意:
Slack Approve ─┐
│
LINE 同意 ─────┼──► accept
│
Web ✅ ─────────┘
最後送回:
{
"id": 77,
"result": {
"decision": "accept"
}
}
這裡 77 代表這個 accept 是在回答剛才第 77 個 Request。
所以完整流程是:
Codex Core
│
│ Need Approval
▼
App Server
│
│ Request #77
▼
LINE / Slack
│
│ 使用者按「同意」
▼
Adapter
│
│ #77 → accept
▼
App Server
│
▼
Codex Core
│
└─ 繼續原本的 Turn
理解 Approval 之後,Tool 就很好理解了。
因為同一種模式延伸成:
Agent → 問 Application
假設 Slack 使用者說:
@codex
幫我找台北車站附近三間咖啡廳。
有時 Google Maps API Key 不想放在 Codex Container。
公司的 Backend 本來就有:
Google Maps API
這時可以反過來讓 Agent 向 Client 要能力。
例如 Agent 判斷:
我需要 search_google_maps
App Server 可以發出 Tool Request:
Codex
↓
App Server
↓
「幫我執行 search_google_maps」
↓
Product Backend
↓
Google Maps API
Backend 查完:
A Cafe
B Coffee
C Cafe
再把結果回去:
Google Maps
↓
Product Backend
↓
Response
↓
App Server
↓
Codex
Agent 拿到結果後,再繼續原本的 Turn。
目前 Dynamic Tool 相關介面仍屬較新的/experimental API,但這個設計很適合用來理解 App Server 想建立的產品邊界。
App Server是一個 Server / Runtime;它對外暴露的是 Codex App Server Protocol,目前本質上是雙向 JSON-RPC 訊息。
而 JSON-RPC 訊息還需要透過某種傳輸方式送出去。
例如 Backend 收到:
{
"type": "message",
"text": "幫我檢查專案"
}
Backend 會先轉成 Codex App Server 的訊息:
{
"id": 10,
"method": "turn/start",
"params": {
"threadId": "xxx",
"input": [...]
}
}
再透過 App Server 支援的 Transport 傳進去。
目前程式裡的 AppServerTransport 定義了:
| Transport | 適合情境 |
|---|---|
stdio:// |
Backend 直接啟動 codex app-server,用 stdin/stdout 溝通 |
unix:// |
同一台 Linux 主機上的不同 Process |
ws://IP:PORT |
不同 Process、Container 或遠端服務之間的長連線 |
off |
不開放外部 Transport |