終端應用需要一個DOM
agent-tui 是一個開源工具,它為終端應用提供了類似瀏覽器的結構化查詢介面,使AI代理能夠像操作網頁元素一樣操作終端螢幕,支援穩定的引用、狀態等待和螢幕區域標識。
在構建C1的軟體工廠Squire時,團隊遇到了一個略顯荒誕的問題:AI工具也是為人類構建的。Squire可以給代理分配工作,但Claude Code、Codex、Pi以及類似的AI框架都以終端應用的形式呈現。它們的即時介面是為人類設計的TUI:一個提示符、流式響應、批准介面、檔案變更面板以及等待下一個指令的游標。另一個代理可以向這個介面輸入內容,但它仍然需要知道響應是否結束、是否出現了批准介面,或者游標是否已經返回到提示符。
這就是agent-tui所解決的問題。它在PTY上執行目標程式,在守護程序中保持終端狀態,將渲染的螢幕作為文本或帶有穩定引用的結構大綱暴露出來,並允許客戶端進行快照、按鍵和等待命名的螢幕狀態。它為終端應用提供了與瀏覽器自動化同樣有用的可查詢表面。
agent-tui是開源的,採用Apache-2.0許可證釋出於GitHub。其設計源於團隊在agent-browser上的經驗:賦予代理可查詢的內容,而非一堆畫素。agent-tui將此理念應用於終端應用。
一個常見的Squire模式是編排代理:一個編碼框架接收任務,然後驅動另一個框架完成工作。在這個演示中,OpenAI的Codex使用agent-tui驅動Pi框架進行真實的終端會話。agent-tui啟動外部的Codex TUI,等待@codex.input,輸入任務並按下回車。然後Codex執行一系列命令,啟動第二個agent-tui守護程序來圍繞Pi。Pi端只是另一個agent-tui會話。結果是在兩個即時螢幕上:一個顯示Codex接收任務,另一個顯示Pi在巢狀會話中回答。
Vercel的AI SDK將框架代理CLI視為特定於提供商的表面,而不是一個通用的包裝器。agent-tui採用相同的方法處理終端螢幕:保留每個應用的形狀,然後暴露代理可以查詢的部分。
為什麼終端應用需要結構?大多數有用的終端程式是為人類而非機器構建的。htop、vim、lazygit、psql、語言REPL以及較新的代理CLI(如Claude Code和Codex)各有不同的任務。它們共享一個自動化問題:即時介面是終端會話。它擁有一個PTY,重繪單元格網格,並期望人們推斷出變化。有些工具提供批處理模式,但許多並不提供。即使存在批處理模式,它通常也是與需要觀察、中斷或引導的人類會話不同的介面。
代理可以輕鬆地向終端寫入位元組。難點在於知道這些位元組落地後發生了什麼。全屏程式可能會原地重繪、移動游標、進入備用螢幕、更新一個欄位,並且從不列印一行乾淨的"就緒"狀態。
通常的選擇會與終端形成糟糕的契約。轉義序列解析將位元組流視為API。渲染文本抓取會丟棄狀態。按鍵之間休眠將問題拋給排程器,這意味著指令碼在CI緩慢或某個提示符進入未匹配狀態時會失效。
給終端一個DOM。Vim是一個很好的壓力測試:它是一個全屏編輯器,而不是列印行的命令。在這裡,agent-tui透過引用而不是休眠來驅動真實的Vim會話。在記錄中,左側窗格發出agent-tui命令,右側窗格是Vim PTY。驅動等待緩衝區,讀取模式,進入插入模式,寫入hello world,儲存hello-world.txt,並檢查檔案內容。
原始的終端文本不適合這個任務。agent-tui暴露了一個大綱:一個帶有角色和穩定引用的螢幕區域樹。引用是螢幕上某物的名稱。它允許代理說"Vim模式指示器"而不是"第24行第1列"。@vim.mode是持久的。無論值是普通、插入還是其他,它都命名螢幕的同一部分。引用可以使用小型選擇器語言進行查詢。內建的vim和shell介面卡會發出命名的引用,如@vim.mode和@shell.prompt。TOML清單可以教agent-tui另一個應用的區域,而無需新增Rust程式碼。即使沒有介面卡,通用介面卡仍然將螢幕分組為粗粒度區域並賦予引用。
團隊參考了瀏覽器自動化的模式,agent-browser已經很好地服務於他們的代理。瀏覽器代理不應點選畫素(412, 308);而應點選按鈕。終端代理不應依賴固定行,而agent-tui可以命名模式、提示符、焦點窗格或表格。
等待螢幕狀態。快照只有在知道何時拍攝時才有用。sleep 0.2是對排程、終端重繪和被測程式的猜測。在快速機器上太長,在負載過重的執行器上太短。更糟糕的是,它與關心的狀態無關。wait子命令與螢幕狀態繫結。它可以等待引用出現、引用消失、選擇器值、正規表示式、事件序列或子程序退出。對於尚未命名的引用螢幕,客戶端可以拍攝快照,保留其螢幕雜湊,傳送輸入,並等待渲染網格發生變化。
以下是完整的vim編輯過程,沒有任何休眠:
- spawn啟動vim
- wait等待緩衝區存在
- press i進入插入模式
- wait等待模式變為插入
- type輸入文字
- press
<Esc>離開插入 - wait等待模式變回普通
- press
:wq儲存退出
每個命令等待下一個可觀察的轉換。按鍵後,指令碼不會假設vim已準備好接收文本。它等待直到解析的模式是插入。離開插入後,它等待直到模式再次變為普通。
引用避免了常見的誤報。正規表示式可以在緩衝區中匹配文字單詞"insert"。等待@vim.mode[value=insert]監視Vim解析的模式欄位。它不是檢視任意的螢幕文本。
也可以等待消失。wait --ref '@vim.cmdline[focused]' --gone阻塞直到命令提示符關閉。對於終端測試,這通常是"可能完成"和"UI狀態改變"之間的區別。
回退到渲染文本。並非每個應用都有介面卡或有用的狀態訊號。htop是一個很好的例子:它沒有JSON模式,有用的輸出通常就是渲染的螢幕。agent-tui可以spawn htop,使用wait --idle 500等待螢幕停止變化,然後以文本模式快照。對於沒有更好訊號的應用,使用--idle;對於有結構的應用,使用引用和選擇器。
那麼tmux和expect呢?明顯的疑問是這是否只是tmux send-keys加上capture-pane,或者是對expect的封裝。它們很有用,但停留在更低的層次。tmux capture-pane提供渲染網格的文本,但不提供角色、命名區域或持久控制代碼。tmux send-keys可以寫入輸入,但對後續螢幕狀態沒有意見。expect是面向行的,適合列印提示符和行的程式,不適合全屏ncurses應用。agent-tui位於位元組流之上和代理之下,讀取終端狀態,為螢幕各部分分配名稱,並允許呼叫者等待這些名稱。
當stdout足夠時使用stdout。並非每個命令都需要即時終端。如果程式已經有非互動模式,正確的介面仍然是stdin、stdout、stderr和退出碼。run子命令用於應返回資料的命令。它為代理提供型別化、帶日誌的包裝,同時PTY自動化專注於即時螢幕。run也面向暴露非互動模式的AI CLI,因此一個模型的答案可以成為另一個步驟的輸入,而無需螢幕抓取提示符。ask是該路徑的簡短封裝。
即時AI CLI會話有兩種路徑。對於一次性答案,使用資料路徑。對於人類終端會話,使用PTY路徑。提供商清單將提示符和響應暴露為螢幕區域。介面卡從渲染單元格命名提示符和響應,而不讀取提供商轉錄API。知道流式答案在跨所有AI CLI中何時結束仍然需要提供商特定的事件或側通道。這種分離很重要,因為它保持兩種情況的分離:當子程序已經是資料產生程序時使用run,當子程序是互動式螢幕時使用spawn、snapshot、press和wait。
捕獲工件。即時會話也會產生檔案。守護程序將每個窗格記錄為asciicast-v3格式,與asciinema生態相容。agent-tui使用相同的cast作為測試輸入。replay不啟動原始程式,而是將記錄的輸出位元組送入新的終端引擎,並比較渲染的快照。演示會話可以成為迴歸測試輸入。對於螢幕截圖,snapshot可以將當前網格渲染為PNG,可選的註釋和邊框用於文件。