Skip to Content

指令 composer

Composer 是掛在聊天室 scope 的編寫通道:你用一般語言描述想要的指令,伺服器端的 agent 幫你起草、對真實資料表驗證、預覽它會做什麼,最後請你核准儲存。它寫出來的就是一般的指令——與你手寫的一樣,走同樣的建立閘門、同樣的授權門檻、同樣的執行語意。

它存在的理由是版本 2 確實是一套 DSL。要手寫一個跨三張表的交易式寫入,你得知道這些表確切的欄位名稱(顯示名稱有歧義時還要知道內部 col_<hex> id)、運算式語法與 tag 邊界。Composer 三者都知道,而它產出的東西仍然會走一般的 POST /commands 建立路徑;編輯既有指令時則走完整的 PUT 取代路徑。

這條通道的形狀

POST .../chatroom/{chatroom_id}/commands/composer/chat → 202 {thread_id, turn_id, poll_path} GET .../chatroom/{chatroom_id}/commands/composer/turn → running | done | error | idle GET .../chatroom/{chatroom_id}/commands/composer/history → 逐字紀錄 + 草稿 + 驗證 DELETE .../chatroom/{chatroom_id}/commands/composer/thread → 丟棄 thread GET .../chatroom/{chatroom_id}/commands/composer/ws/docs → WebSocket 契約

只有聊天室 scope。沒有部門或公司版本:它是為呼叫者以已加入成員或符合資格的部門/公司管理員身分可存取的聊天室編寫指令。有狀態的 REST routes(/chat、/turn、/history、/thread)都使用 ChatRoomJoinedRequired;靜態的 /ws/docs 契約 route 只要求 JWT,不檢查聊天室存取資格。

Thread 以 client_id 為 key

每個有狀態的 REST 呼叫都要帶 client_id(^[A-Za-z0-9_-]{1,64}$)——每個裝置或瀏覽器分頁一個。thread id 由 server 從公司、聊天室、使用者與這個 client_id 推導,所以訊息裡的任何內容都不可能選到別的租戶、聊天室或使用者的 thread。兩個分頁就是兩段各自獨立、各有草稿的對話,不會合併。WebSocket 的 client_id query parameter 則可省略:依序退回 signature payload 裡的 client_id,再退回穩定、由 signature 推導的 ws-… id。

POST /chat 是非同步的

一次回合要 30–120 秒,因為 agent 會派發 subagent 並做上數十次工具呼叫——比 API 前方 edge 願意保持同步回應的時間還長(Cloudflare 約 100 秒就回 524)。所以這個 route 跑完快速閘門、排程回合,然後回 202:

{ "thread_id": "ctcomposer:…", "turn_id": "0f4c1b7a9e2d4a6b8c0d2e4f6a8b0c1d", "state": "running", "poll_path": "/private/module/custom_tables/chatroom/…/commands/composer/turn?client_id=tab-1&turn_id=0f4c…", "poll_after_ms": 2000 }

接著用 &wait_seconds=30 long-poll poll_path,直到 state 離開 running。這個 route 可能拋出的每一種 4xx 仍然是同步的:

以下每個 body 都包在 detail 裡。

狀態detail原因
403純字串呼叫者未通過聊天室的已加入成員/管理員存取檢查(例如 Insufficient permissions, not a member (Required department manager).),或 composer 類別在適用的公司、部門、聊天室或使用者 quota 中有一項已用盡(Budget exceeded for chatroom category=custom_table_composer (…))。UserAPIKey 呼叫者會得到 Custom-table command routes require JWT authentication。
404Chatroom Not found聊天室不存在或已被軟刪除。
409{"error":"job_model_not_configured","job":"custom_table_command_composer","pin_path":"/root/model-catalog/job-models/custom_table_command_composer"}這個部署沒有替 composer job 釘選模型,而且沒有預設值。由 operator 以 PUT 呼叫 pin_path 釘選後再重試。
409{"error":"turn_in_progress","message":"Another turn is already in progress on this thread."}同一個 client_id 還有回合在跑。請輪詢 GET /turn,或 DELETE /thread。
409{"error":"no_pending_approval","message":"This thread has no save waiting for approval."}沒有待審的儲存卻送了 resume。
422{"error":"composer_model_unavailable","code":…}釘選的模型被政策拒絕或無法建構(有 code 時會說明原因)。
422validation errorBody 形狀問題——包含 send exactly one of message or resume。
503{"error":"composer_unavailable","message":"Composer checkpoint storage is not configured on this deployment."}這個部署尚未設定 composer 的 checkpoint 儲存。

message 或 resume,不能同時

message 是跟 agent 對話。resume 是回答 agent 正在等待的儲存:{"decision":"approve"} 會把草稿存下來,{"decision":"reject","message":"…"} 則把理由交回給模型。兩個都送或兩個都不送都是 422。

在儲存待審期間送出一般的 message,等於退回那次儲存,並把你的訊息當成理由。它不會排在核准後面,也不是一則新的請求。想要核准請送 resume。

GET /turn——四種狀態

state意義
running相符的紀錄仍在執行,因此 elapsed_seconds 會增加;或者紀錄已遺失/被取代但 thread lock 仍被持有,此時時間欄位可能缺少。請繼續輪詢。
doneresult 就是回合結果(見下)。
error回合在產生結果前就遺失。error.error 為 turn_lost(worker 中途死亡——用 DELETE /thread 重置)、turn_cancelled 或 internal_error。
idle這個 thread 沒有追蹤中的回合:從未開始、已被後續回合取代,或紀錄已過期。逐字紀錄仍在 GET /history。

帶上 202 handle 裡的 turn_id,就絕不會拿較新回合的結果頂替;已被取代的 id 會在目前的 thread lock 被持有時回 running,之後回 idle。不帶則拿到 thread 最新的回合。wait_seconds 接受 0–60(預設 0);更大的值會回 422,而不是被截成 60。JWT 與成員資格 dependency 通過後,狀態 handler 只讀 Redis 的回合紀錄與鎖,不會開啟 composer checkpoint Postgres,因此可以安全輪詢。

idle 是最容易被誤呈現的狀態:六小時前結束的 thread 會回 idle 而不是 done,因為回合紀錄會過期,但對話還留在 checkpoint。請改讀 GET /history,不要顯示「什麼都沒發生」。

一個回合的付費工作上限為 1500 秒,超過就以 failed 結束。超過 1620 秒(上限加上 120 秒寬限)仍為 running 的紀錄,會回報為 {"state":"error","error":{"status":504,"error":"turn_lost","message":"The turn never reported a result; DELETE /thread resets the thread."}}。Per-thread lock 是最多保留 1800 秒的 Redis key,所以已死的 worker 會讓它的 client_id 持續回 409 turn_in_progress,直到鎖過期或 DELETE /thread 清掉它;Redis 本身出錯時,鎖會 fail open,回合就可能重疊。因程序關閉或 WebSocket handler 被取消而中斷的回合會記錄為 {"status":503,"error":"turn_cancelled","message":"The turn was cancelled before it finished."},已寫入 checkpoint 的進度會保留。failed 的回合一律帶固定訊息「這一輪處理失敗,請再試一次或換個說法。」。在同一回合中 save_command 已經 commit 之後才發生的 graph 失敗或逾時,仍會回報 saved,而不是 failed。

回合結果

result 會帶四種狀態之一。WebSocket turn_result frame 帶有相同的結果欄位,另外多一個 frame discriminator:"event":"turn_result";因此它與 REST object 並非逐 byte 相同。

status你要呈現什麼
answeredassistant_message,加上 thread 目前的 draft/validation/example(thread 還沒有時各為 null——先前回合的草稿會再次出現)與 todos。
awaiting_approvalpending_approval——儲存確認卡:name、visibility、agent_enabled、table_ids、agent_tool_name。
savedsaved——command_id、name、visibility、agent_enabled、agent_tool_name、execute_path。
failedassistant_message 是固定的重試訊息,terminal result 裡的 draft/validation/example 為 null;請用 GET /history 讀 checkpoint 狀態,失敗前已完成的變更可能仍在。

draft 是一整份 CommandCreateRequest 再加上可選的 command_id(在編輯既有指令時會設定,此時儲存是整筆取代)。validation 與 POST /commands/validate 回傳的 CommandValidationReport 形狀相同,但它是 agent 讀回來的那一份:每個字串都經過淨化(壓成一行、&、<、> 轉成 HTML 實體、反引號被替換、最多 4,000 字元),所以訊息文字可能與直接呼叫 validate 不同。todos 是 agent 自己的計畫,很適合當進度清單。

核准後的儲存會以你的身分走一般 authoring 閘門,所以它可能和一般的 POST /commands 建立或完整 PUT 取代一樣失敗——無法管理的資料表、違反 tag、名稱衝突。Composer 是編寫助手,不是權限的旁路。

編輯既有的 command

load_command 會把呼叫者可以編寫的 command 載入草稿(此時會設定 draft.command_id,儲存就是完整取代);其他 id 一律回 not_found。Callback 的機密永遠不會送到模型:URL callback 的 url 與每個 header 值都會以 "<REDACTED>" 回傳、body 則為 {"redacted":"<REDACTED>"};API tool callback 保留 api_tool_id,body 同樣遮蔽。儲存時,每個沒被動過的佔位值都會依 callback 名稱加 header 名稱,從現行 command 還原;被改寫的值則照寫入的內容儲存。沒有對應值可還原的佔位值——callback 改名、新增的 header 名稱,或把載入的草稿另存為新 command——會在寫入任何資料前拒絕儲存:save_command 回傳 {"saved":false,"errors":[{"status":400,"detail":{"error":"unrestorable_callback_secret","message":"<text>"}}]},訊息為 callback '<name>' redacted url requires an existing live value、… redacted header '<header>' …、… redacted body … 或 … redacted body value …,回合以 answered 結束,所以只有在同時提供新值時才改 callback 名稱。

自後端 5.22.0 起,save_command 也會拒絕宣告了 attachment_copies 的草稿,以及編輯已帶這類 grant 的 Command(不論草稿保留或拿掉它),因為核准卡不顯示 grant。結果是 {"saved":false,"errors":[{"status":422,"detail":{"error":"attachment_copies_rest_only","path":"definition.attachment_copies","message":"<text>"}}]},不會儲存、也不會重新蓋章,grant 要改走 Commands REST API 宣告或編輯。見附件複製。

preview_example(inputs) 會拿範例 input 驗證草稿,但不執行任何東西;回合結果與歷史中的 example 就是它最後一次的輸出,{inputs, mode, plan | walkthrough, advisories}——版本 1 草稿顯示綁定後的 plan(每個展開的 action 一筆),版本 2 草稿則顯示 step 的 walkthrough(每個 step 一筆 {kind, name, table})。草稿沒有撰寫 examples、而預覽過的 input 仍然有效時,儲存會把它存成 definition.examples = [{"name": "範例", "inputs": …}],成為 agent 工具的範例呼叫;過時的預覽會被靜默丟棄。

成功儲存後,server 會 best-effort 嘗試丟棄 thread。清理成功時,緊接著呼叫 GET /history 會拿到空的 messages;checkpoint/Redis 清理失敗不會改變 saved 結果,而且可能留下舊歷史。請以 status: "saved" 判斷儲存結果,不要以空逐字紀錄判斷。

GET /history 與 DELETE /thread

GET /history 回傳逐字紀錄(有文字的 user 與 assistant 回合,最後 500 筆)、目前的 draft、最後一份 validation,以及有待審儲存時的 pending_approval。工具呼叫、工具結果與 subagent 逐字紀錄一律不外露。

DELETE /thread 會丟棄 thread:checkpoints、thread 索引項目,以及崩潰回合留下的 turn lock。清除鎖是 best-effort 且絕不拋錯,所以即使失敗的是 checkpointer 操作本身,它仍會釋放 client_id——而那正是你需要它的時候。

DELETE /thread 不會取消已經在執行的 worker。只在你確定回合已死亡時用它復原;若清掉活回合的鎖,舊回合仍可能完成——甚至仍可能儲存指令——同時新的回合已在同一個 client_id 上開始。這個 endpoint 刪的是對話狀態,不是指令資料列;已經儲存的東西不受影響。

WebSocket 通道

GET .../composer/ws/docs 以型別化的 payload 公開 socket 契約,因為 WebSocket route 不會出現在 OpenAPI。請以該 route 為準,不要把協定寫死;下面只是概覽。

  • Socket 位址是 /ws/custom-tables/composer?signature={signature}&client_id={client_id}。
  • 它不是用 Bearer token,而是用 POST /private/get_signature/{chatroom_id} 發出的短期 signature:24 小時滑動 TTL、7 天絕對上限,並且每 300 秒重新檢查 signature 與同一套已加入成員/管理員聊天室存取條件——閒置的 socket 也一樣。
  • Client frame:message、resume、action:history、action:delete_thread、action:ping。
  • Server event:connected、turn_started、token、tool_call_start、tool_call_end、todos、heartbeat、approval_required、turn_result、history、thread_deleted、pong、error。
  • Close code:invalid_signature、invalid_client_id、not_a_member,以及累積 20 個 malformed frames 後的 invalid_frame 為 1008;internal_error 為 1011;composer_unavailable 為 1013。v5.10.11 的 live socket 會送出 invalid_client_id,但 /ws/docs 的 close_codes 清單漏了這一項。

一般工具呼叫進度 frame 只帶名稱、id、成功旗標與參數名稱摘要——不帶參數值或工具輸出。task 派發是例外:subagent 型別與模型所寫 description 的合併摘要上限為 200 字元,因此請把該欄位視為模型文字。回合執行期間每 20 秒會有一個 heartbeat(subagent 工作期間不會產生其他 frame);請把它當成存活訊號,不要當錯誤。回合中途斷線不會中止回合:重新連線後送 action:history,或輪詢 GET /turn 取得結果。

正常情況下,per-thread lock 讓兩條通道在每個 client_id 同時只能有一次回合;並行回合會拿到 409 turn_in_progress。刻意的例外是 DELETE /thread:如上所述,它會清鎖,但不會取消活著的 worker。

工作階段內的錯誤 frame 是 {"event":"error","status","error","message"},composer_model_unavailable 另帶 code,409 job_model_not_configured 另帶 job/pin_path,socket 保持開啟。純字串的 4xx——聊天室存取或預算的 403——會以 "error":"request_rejected" 送出,字串放在 message。格式錯誤的 frame 是 {"event":"error","status":422,"error":"invalid_frame","message":"Send {message} | {resume:{decision,message?}} | {action: history|delete_thread|ping}."},第二十次會以 1008 關閉 socket。交握與定期重新檢查時的拒絕(invalid_signature、invalid_client_id、not_a_member、internal_error、composer_unavailable)會送出不帶 status 的 {"event":"error","error","message"},然後關閉。申請 signature 本身需要真正的聊天室成員資格:不是成員的管理者可以用 REST,但 POST /private/get_signature/{chatroom_id} 會回 400 User is not a member of the chatroom。

Agent 能讀取與修改什麼

Composer 是一個主 agent(ct_command_composer),透過 task 派出兩個子 agent:schema-analyst(讀取工具)與 dsl-builder(編輯草稿、驗證、預覽與接線 API tool);通用的 general-purpose 子 agent 已停用,save_command 會暫停等待核准。每個回合,主 agent 最多 16 次模型呼叫與 40 次工具呼叫,每次子 agent 執行最多 12 次模型呼叫;達到模型呼叫上限時該次執行就結束。只有主 agent 的呼叫會出現在 tool_call_* frame 中。

每次工具呼叫都會開一個新的 session,重新確認使用者仍存在於同一家公司並通過聊天室存取檢查;失敗時模型會收到 {"error":"denied","message":"You are not allowed to use the composer in this chatroom."},非預期錯誤則是 tool_failed。工具結果的字串上限 4000 字元、集合上限 100 筆。

工具回傳內容
list_tables(name_contains?, offset, limit)此聊天室中呼叫者所管理的資料表,以 limit 1–20(預設 20)分頁。它只會先掃描聊天室最近建立的 500 張資料表再過濾,並回報 truncated: true 與 total_managed,所以大聊天室裡較舊的資料表可以用名稱找到。每筆為 {id, name, description, tag_ids, column_count, approval_gated}(moderator 另外會有下方的 trigger 旗標)。
get_table_schema(table_ids)只取前 10 個 id。回傳欄位顯示名稱、型別、必填旗標、link 目標與計算欄位旗標,以及審批規則;呼叫者不管理或屬於其他聊天室的資料表會顯示 not found。
list_tags聊天室的每個 tag 與其成員資料表 id。
list_commands(name_contains?)呼叫者透過 REST 可列出的前 200 個 command,再經過濾;最多 100 筆會送到模型。
list_api_tools/wire_api_tool只限聊天室管理者(聊天室建立者、同部門管理者或公司管理者)——一般成員會得到 forbidden。模型看得到每個工具的 id、名稱、method、去掉查詢字串與 fragment 的 URL(最多 300 字元)、排序後的 header 名稱、最上層 body key,以及可否接線。wire_api_tool(tool_id, on, callback_name, body) 會把 callback 與其生命週期 trigger 加進草稿。

Agent 能讀到哪些 trigger 資訊

撰寫權限走的是資料表管理者檢查,這也會放行不是表格 moderator 的聊天室管理者。但 trigger 設定屬於 moderator 的資料,所以 composer 的 list_tables 與 get_table_schema 工具會套用與 GET .../triggers 相同的 moderator 閘門。不是表格 moderator 的呼叫者完全拿不到 trigger 資訊:list_tables 省略 has_invoke_command_trigger,get_table_schema 回傳 invoke_command_triggers: []。Moderator 只拿到投影——含有 invoke_command action 的 trigger,每個以 {id, name, on, actions} 呈現,其他 action 縮減成 {type},invoke_command action 則是 {type, command_id, inputs}。剛好等於 $row.<欄位> 的 input 以顯示名稱呈現,其他 input 值一律顯示 __SECRET__。任何 URL、header、body、訊息或收件者都不會進入模型。

Operator 前置作業

Composer 需要替 custom_table_command_composer job 釘選模型,這由 operator 透過 root route 完成,沒有預設值。在釘選之前,POST /chat 會回 409 job_model_not_configured,WebSocket 的回合則會收到 status: 409、帶相同 job/pin_path 欄位的 error frame,而 GET /history、GET /turn、DELETE /thread 以及 socket 的 history 與 delete 動作照常運作。

Composer 的對話狀態需要一套 checkpoint store,部署不一定有設定。未設定時,POST /chat、GET /history、DELETE /thread 會回 503 composer_unavailable,實際 WebSocket handshake 則以 1013 關閉;GET /turn 只需要 Redis 的回合紀錄,靜態的 GET /ws/docs 兩種 store 都不需要。有兩個 root operator route 負責 checkpoint 狀態。它們僅限 operator、需 root 憑證,在 root 參考頁列出只是作為背景說明,不是前端整合點。

延伸閱讀

Last updated on