執行與取回結果
單一 run 操作以聊天室定址(跨房間歷史另有 GET /runs):/chatrooms/{chatroom_id}/...。受限 API key 可用 menu / 提交 / 查自己的 run / 下載自己的產出物 / 取消自己的 run;retry 對受限 key 在允許房間的 route allowlist 回 405,quick-run 在 auth 回 403(進到 router 則 404)。
1. 探索可執行清單
GET /chatrooms/{chatroom_id}/menu # query page_size (ge1 le100, 預設 50)回 items[](SandboxMenuItemResponse):task_id、task_name、task_version_id、task_version_digest、requires_confirmation、secret_slot_names、必填的 source_visible=false、secret_slots[](逐 slot 的 borrower/owner 履行狀態)、timeout_seconds、update_available、input_instructions / input_example / input_schema、runtime_contract、secret_use_risk_warning?。只有已授予且已啟用。每個呼叫者同一份清單。 不含腳本原文——這個選單回應上的 runtime_contract.driver 永遠是預設 fallback ". /workspace/work.sh",任何呼叫者(包括 owner)都看不到真正的 work_command(真正的 driver 只會出現在 GET /tasks/{id}/versions/{vid} 或 POST .../input-preview)。page_size 預設 50;下一頁存在時,next_page_token 是不透明 cursor。房間管理者用 granted-jobs 改目錄。
提交前先在 client 端用 input_schema(若有)驗證 input——伺服器端也會強制檢查(422 input_schema_violation,在 dispatch 之前)。POST /tasks/{task_id}/versions/{vid}/input-preview 可以讓作者先試跑一個候選 input,看清楚 work script 實際會讀到的位元組。見 任務、發布與綁定。
2. 提交執行
POST /chatrooms/{chatroom_id}/runs
Idempotency-Key: <UUID> # 必帶,否則 422 idempotency_key_required
{ "task_version_id": "...", "input": {...}, "timeout_seconds": 1800 }input:canonical JSON(≤1 MiB、深度 ≤64、節點 ≤100k、無 NUL、無重複 key)。- body 過大先回 413
request_too_large(依 Content-Length)。 - 版本非「published + content active」→ 409
version_not_runnable(content_state為scrubbed時則是 409content_scrubbed_irreversible;_refuse_not_runnable只是後端內部函式名,不是回應碼)。 - 回
SandboxRunDetailResponse,status: "queued"。 - 手動 REST 需要 live share + 成員資格(
can_execute_manually)。不要求啟用。產品 UI 仍應提交選單上的task_version_id。Agent 提交需要啟用。 - run 的
input會先暫存為 owned object,但不再計入公司每分鐘 5 次上傳起始的額度(後端 ≥ #1140);連續提交會全部以queued接受。422input_not_stageable現在只代表 input JSON 無法 canonicalise 或寫入。 - **佇列背壓(429,什麼都沒排入)。**OpenAPI 在提交、重試與 Quick Run 上宣告為
SandboxQueuedRunLimitErrorResponse/SandboxQueueFullErrorResponse,兩者都包在detail裡。sandbox_queued_run_limit表示公司等待中的 run 已達上限(body 的limit);退避到等待中的 run 變少再送。queue_full只會出現在地端部署:整個 executor fleet 的佇列已滿,body 帶limit與retry_after_seconds,同一個值(60 秒)也放在Retry-After。等過這段時間後,用同一把Idempotency-Key重送。見雲端與地端部署。 - 託管雲端上,提交後幾秒內就會派送(API 在 commit 後喚醒 controller;另有 3 秒一次的掃描當後備)。剛發布版本的第一次執行要在 Cloud Run 拉映像(
wait_reason: container_starting,30–90 秒);之後每次約 10–30 秒到running。
3. 輪詢狀態
GET /chatrooms/{chatroom_id}/runs/{run_id}status 走 queued → starting → running → completed(或 customer_failed / platform_failed;取消 cancel_requested → cancelled)。終態看 terminal_at。
每個終態 run 都帶租戶可見的失敗來源(completed 時為空):
| 欄位 | 意義 |
|---|---|
error_code | 固定 slug,例如 work_exit_nonzero、work_timeout、invalid_output、cancelled_by_request、binding_revoked、interrupted_by_platform、result_upload_failed(工作成功但平台無法儲存輸出——platform_failed,請重試) |
exit_code | 工作指令真正的結束碼(exit 3 就是 3;124 逾時、130 取消、127 找不到指令) |
failure_stage | task_bundle / secret_env / workspace / spawn / metering_* / work / output / timeout / cancel / egress_cap / wait |
failure_source | runner / reconcile / dispatch / submit / policy / tenant |
error_detail | 已消毒的簡短說明([A-Za-z0-9._@:/+=%;-],≤128) |
failure_hint | 失敗終態附帶的修法建議,由 error_code/exit_code 推出(例如 exit 127 → 把工具鏈目錄加進 PATH、work_timeout → 調高 timeout_seconds)(後端 ≥ #1140) |
measured_ingress_bytes、measured_egress_bytes | 工作階段計量的網路位元組 |
每一次非終態的輪詢都會說明正在發生什麼與該做什麼(後端 ≥ #1140),請直接呈現給等待中的使用者:
| 欄位 | 意義 |
|---|---|
wait_reason | 封閉詞彙:awaiting_dispatch、company_saturated、no_active_region、missing_image_digest、capacity_snapshot_stale、provider_capacity_exhausted、provider_submit_pending、provider_submit_uncertain、container_starting、cancel_pending;running 與所有終態為空 |
next_action | 對應 wait_reason 的白話指引(要等還是要動手) |
wait_since | 目前這段等待從何時開始(排隊中為 queued_at,啟動中為 started_at) |
queue_position | 在 run 所等待佇列中的位置(從 1 起算);已被認領或派送、或根本沒在等待時為 null。雲端:在公司 queued run 中的位置。地端:跨公司的 executor 佇列位置,executor 接手前的 starting 也算 |
eta_seconds | 等待中的 run 大約還要幾秒開始,或 null。託管雲端一律 null。地端:ceil(queue_position ÷ 整個 fleet 的 run slot 數) × 同一 resource profile 最近 20 筆已完成 run 的平均耗時;沒有這些歷史時為 null。有值才顯示,絕不自行推算 |
給使用者看 error_code + exit_code,並提供 log(§5a)——log 裡有腳本的 stdout/stderr,例如 /workspace/work.sh: line 1: python3: not found。
secret_env_name_collision/secret_env_value_collision(platform_failed,stage secret_env)表示某個 ordinary_env 變數與已綁定的 secret slot 同名,或其值與已綁定的 secret 值相同。Runner 在工作開始前就拒絕,error_detail 為空,使用者只看得到 failure_hint:把變數或 slot 改名讓兩組名稱不重疊,或移除該變數、改從 slot 讀取 secret。
列表 + 篩選:
GET /chatrooms/{chatroom_id}/runs?status=queued&status=running # status 可重複受限 key 和 department-manager 角色以下的一般使用者都只會看到自己提交的 run;整個房間的清單需要 department manager(或 company manager)以上的角色,且即使是管理者,每一列仍會經過 can_read_run 把關。
4. 看有哪些產出
GET /chatrooms/{chatroom_id}/runs/{run_id}/content
# → { state, has_input, has_output, has_log, has_artifact_bundle, input_digest }只有布林旗標,沒有內容本體(這是「c2 log/output」的 metadata 視圖)。state 可能是 "missing"(尚無內容列)。
Runner 的結果上傳不受作者上傳初始化次數預算限制。除了 work exit code,仍須分別檢查 has_output、has_log 與實際下載;result_upload_failed 是平台失敗,請依 failure_hint 處理。
5. 列出並下載產出物
GET /chatrooms/{chatroom_id}/runs/{run_id}/artifacts # 預設 lifecycle=active
# → items[] { id, relative_path, byte_size, digest, deletion_state, declared_content_type }
POST /chatrooms/{chatroom_id}/runs/{run_id}/artifacts/{artifact_id}/download
# → { capability_token, expires_at, content_type, content_disposition, x_content_type_options: "nosniff" }- 怎麼產生產出物檔案(runner ≥ #1152,2026-09-04 已在 staging 實測:兩個檔案、列表、逐檔公開連結、撤銷):工作把檔案寫到
$TEAMSYNC_ARTIFACTS_DIR(/workspace/artifacts,一開始是空的;子目錄變成路徑前綴)。工作結束後 runner 把裡面每個一般檔案打成一個確定性的 bundle;之後GET .../content會回has_artifact_bundle=true,GET .../artifacts逐檔列出relative_path、byte_size、digest。遇到 symlink/hard link/特殊檔、..、超過 1000 個檔案、路徑過長、或總量超過 standard profile 上限(workspace 上限 − 16 MiB,最多 1 GiB)時整包拒收——run 照樣 completed,log 會寫stage=artifacts refused: <原因>。output.json仍是結構化結果。 - 回的是短效
capability_token(TTL 5 分鐘),不是原始 key / presigned URL。這個 POST 只做擁有權授權(產出物的deletion_state必須是active)——沒有端點會兌換這個 token。要取單一檔案的位元組,用逐檔公開連結(後端 ≥ #1149):POST .../runs/{run_id}/artifacts/{artifact_id}/public-link→{ url, token };GET <url>驗過sha256後只串流該檔案在 bundle 裡的位元組區間,同路由DELETE撤銷,產出物或 bundle 退場後連結自動 404。只限 owner-manager(受限金鑰 404)。 deletion_state != active的產出物,download 一律 404。- 要把一次執行的內容交出去,用 §5a 的
POST .../runs/{run_id}/{log|output}/public-link(GET /public/sandbox/artifacts/{token});Agent 工具sandbox_job_status另可回有界、不可信的預覽(output ≤64 KiB、log 頭尾 ≤16 KiB)。
5a. 把一次執行的 log/output 交給 TeamSync 之外的人(可選)
POST /chatrooms/{chatroom_id}/runs/{run_id}/{log|output}/public-link
# → { run_id, object_kind, url, token, revoked, created_at }這是永久的(沒有 TTL),連結一旦鑄出就不必登入——url 是 GET /public/sandbox/artifacts/{token},不需要登入。鑄造冪等;DELETE 同一條路撤銷(要新 token 再鑄一次)。只有 log/output,不含個別產出物。完整契約見 內容、Digest 與下載。
6. 取消 / 重試
POST /chatrooms/{chatroom_id}/runs/{run_id}/cancel # 取消你讀得到的 run(受限金鑰:只有自己的;kill switch 下仍可用)
POST /chatrooms/{chatroom_id}/runs/{run_id}/retry # 以新身分重試終態 run;帶 Idempotency-Key;受限 key 在允許房間回 405(若到 router 才是 404)- retry 允許任何終態:
completed/customer_failed/platform_failed/cancelled。永遠是新的run_id。受限 key 在允許房間回 405(若到 router 才是 404)。同一把Idempotency-Key+ 不同 request hash → 409idempotency_conflict。 - Retry 保留精確的任務版本 pin,建立新的 run 身分。省略 body、送
{}或{"input": null}會逐位元重放保留的來源 input;非 null 的input則覆寫參數,依一般輸入限制重新驗證與暫存。來源 input 不存在、已退役、無法讀取或 digest 不符時,重放回 409retry_input_unavailable,請改傳明確的 input。必帶Idempotency-Key;來源未終態回 409retry_not_eligible。受限 Sandbox key 不可 retry:允許房間的請求會被 route allowlist 拒絕(405);不在 key scope 的房間可能更早回 403。 - 來源 run 還不是終態 → 409
retry_not_eligible。 - 未認領取消(
queued):立刻變成cancelled,釋放定價 hold,也立刻釋放排隊/並行容量 slot(2026-08-30 修正——之前一次競速中的並行 dispatch 可能讓已取消的 run 洩漏容量 slot)。已認領/執行中取消:cancel_requested,再由 runner 走到cancelled;租戶取消圍欄贏了時error_code=cancelled_by_request。cancel_requested_at有持久化,不回傳。 - 極少數情況下,若控制平面在 dispatch 途中掛掉,run 可能卡在
starting;背景的 reaper 會自我修復(重新驅動同一次 dispatch,且只會執行一次)——不需要租戶動作,取消在這期間仍然可用。 submission_source是rest(本頁)、quick_run、agent或custom_table_trigger。四條路的輪詢/下載契約相同。
Quick Run(manager-only)
一次原子建立隱藏的聊天室擁有父任務 + 版本 + 執行:POST /chatrooms/{chatroom_id}/quick-run(SandboxQuickRunRequest,帶 Idempotency-Key)。回 { task, version, run }。受限 key 在 auth 回 403(若到 router 才是 404)。不要把那個 owner_scope 抄到 POST /tasks。
Quick Run 跟手動提交一樣暫存 input(v5.10.11;在此之前工作讀到空的 /workspace/input.json,GET .../content 也回 has_input=false)。因此在 manager 檢查與冪等重放之後,可能回 422 input_not_stageable/503 input_staging_unavailable,此時尚未建立任何任務或 run。Quick Run 不受公司政策的 queued-run 上限檢查;地端部署的每公司與 fleet 佇列上限仍然適用(429 sandbox_queued_run_limit/queue_full)。
Company manager 每任務歷史
不是房間清單。只有 company manager:
GET /tasks/{task_id}/runs?offset=0&limit=10&order=desc
GET /tasks/{task_id}/runs/numOfData兩邊同一組篩選:status、method(agent|manual)、executor_kind(internal_user|external_user)、source_kind(chatroom|department|external_platform)、department_id、chatroom_id。列表回應是歷史列的 JSON 陣列(duration、已結算 cost_usd、執行者、房間、部門、method)— 不是 { items, page_token }。numOfData 是 { num }。受限金鑰在認證層直接 403(路徑在 restricted-key 拒絕清單上,也不在 Sandbox scope 允許清單內),永遠到不了 router 的 404。
常見錯誤速查
| 狀況 | 回應 |
|---|---|
| 缺 Idempotency-Key | 422 idempotency_key_required |
| body 過大 | 413 request_too_large |
| 版本不可執行 | 409 version_not_runnable |
| retry 來源不合格 | 409 retry_not_eligible |
| 未授權 / 受限 key 動了禁區 / 產出物非 active | 404 |
| kill switch 下的變更 | 503 sandbox_disabled(/cancel、/download 除外) |
| Envelope 塞不進配額 | 403 budget_reservation_exceeded(不建列) |
| 公司 queued-run 上限已滿 | 429 sandbox_queued_run_limit(body 帶 limit;預設 100,可由公司政策覆寫;地端取它與主機每公司上限(預設 20)中較小者)——退避後再送,不要熱重試 |
| 地端 executor fleet 佇列已滿 | 429 queue_full(body 帶 limit、retry_after_seconds;Retry-After: 60)——等過這段時間再重試 |
定價拒絕(no_active_region …) | 422 |