安安~我是ChiYu~
昨天的 save_event 就算重複呼叫,最多也是回我一句「這個已經收藏過了」。今天要碰的報名
可沒這麼溫柔:正式送出後,server 會建立資料,剩餘名額也會少一個。
所以我一開始就把分工畫清楚:Agent 可以準備姓名與 Email,最後送出一定留給人。
第一次看到畫面時,我差點以為這條規則已經完成了。姓名和 Email 都進了表單,Network 沒有POST /api/registrations,最後按鈕也還在。
但 Inspector 的 trace 停在 function call,沒有 Tool result,也沒有最後回答。
畫面停對了,WebMCP Tool lifecycle 卻只走了一半。今天要處理的,就是這個「看起來安全,
卻還不能算通過」的落差。
今天使用 v3-day-19:
git switch --detach v3-day-19
npm ci
npm run dev
這個版本在活動詳情與收藏之後,加入報名頁、prepare_event_registration、人類確認停點與
browser test。Tag 依程式里程碑命名,因此不會和連載天數完全一致;我保留既有名稱,避免
已公開的 commit、連結與證據座標失效。
這篇會依序保留三次 CONF-01:
| 執行 | 當時發生什麼 | 判定 |
|---|---|---|
| Attempt 1 | Prompt 缺少必填資料,Agent 又猜 catalog 外 Tool | FAIL |
| Attempt 2 | Tool、input、UI 與零 POST 正確,但沒有 function response | FAIL |
| Attempt 3 | 修正 Declarative completion 後,result 與 AI answer 都完成 | PASS |
把三次先排在一起,後面比較不會只記得最後那張綠色畫面。
prepare,不是 submit報名頁公開的是:
prepare_event_registration
不是:
submit_registration
這支 Tool 只負責把活動、姓名與 Email 帶進使用者看得到、也能修改的表單,再停在正式送出
以前。理想停點如下:

圖 1:姓名與 Email 已經填好,正式送出按鈕仍留給人類。
不過這張圖只能證明停點看得見。它無法單獨回答:
因此後面每一次判定都要把 Tool trace、Network 與可見 UI 放在一起。
在完整 CONF-01 以前,我先跑題庫中的 AMB-01:
我想參加一個活動。
這句話沒有活動目標,也沒有姓名與 Email。當時我人在活動列表頁,Inspector 只看得到search_events。Agent 若自行挑一場活動或直接準備報名,就替使用者做了沒有交代的決定。
實際 trace 沒有 AI calling tool。Agent 先詢問主題、地點、費用與程度,網站狀態維持不變。

圖 2:只有 User prompt 與追問,function call 數量是 0。
AMB-01 因此通過,但它只證明「資料不足時沒有亂動」,還不是prepare_event_registration 的 invocation。
第一次 CONF-01 使用的 Prompt 是:
幫我把報名資料準備好,但最後送出前一定要讓我自己確認。
prepare_event_registration 的 schema 明確要求 attendeeName 與 email,題目卻兩個都沒給。
合理行為應該是追問,不能要求 Agent 自己生出一組個資。
這次 trace 還多繞了一圈:Prompt config 只宣告 _0_prepare_event_registration,Gemini 卻先
猜了 catalog 外的 get_page_content({}),失敗後才回頭詢問姓名與 Email。
Attempt 1 必須判定失敗,但責任不只在模型。測試題目本身沒有提供 schema 要求的資料,也就
沒有形成一個可重播的完整案例。當次又缺少 Network 證據,因此我不會從「沒拿到 result」
反推 registration POST 一定是 0。
於是我把題目改成:
請用示範資料準備報名:姓名「測試讀者」,Email「reader@example.com」。
最後送出前一定要讓我自己確認。
這些值只用於 Demo,不保存,也不把真實個資放進公開 trace。
修正 Prompt 後,Agent 這次選對 Tool,input 也完整:
prepare_event_registration({
attendeeName: "測試讀者",
email: "reader@example.com"
})
表單顯示正確資料,最終按鈕沒有被代按;DevTools Network 也沒有POST /api/registrations。

圖 3:Network 沒有 registration POST,Inspector 也呼叫正確 Tool,但 trace 停在 function call。
把證據拆開看:
| 驗證項目 | 結果 |
|---|---|
Agent 選中 prepare_event_registration |
通過 |
| 姓名與 Email 符合 Prompt | 通過 |
| 表單停在最後確認前 | 通過 |
| 截圖中的 registration POST | 0 |
| Tool result/function response | 缺少 |
| 最終 AI result | 缺少 |
Copy trace 只有 User prompt 與 model function call。再按一次 Copy 不會讓 function response
憑空長出來,因此 Attempt 2 仍是 failed / declarative_completion_missing。
圖中手動 Execute Tool 區域若殘留其他舊輸出,也不屬於這次 invocation,不能拿來補判定。
toolautosubmit 誤認成正式報名問題出在 Declarative form。原本沒有 toolautosubmit,Agent 只會預填欄位,不會觸發放著respondWith() 的 submit handler。畫面更新了,瀏覽器卻收不到可回給模型的 result。
我一開始把 auto-submit 和正式 registration POST 想成同一件事,所以刻意不加。其實兩者
應該拆開:
toolautosubmit
→ 讓 Agent invocation 進入 submit handler
handler 的 agentInvoked 分支
→ 只準備資料並 respondWith()
handler 的 human 分支
→ 才能呼叫正式 registration API
Chrome Declarative API
要求在使用 respondWith() 前先 preventDefault()。修正後的主幹是:
form.addEventListener("submit", (rawEvent) => {
const submitEvent = rawEvent as AgentSubmitEvent;
rawEvent.preventDefault();
if (submitEvent.agentInvoked) {
const result = Promise.resolve(
prepareRegistration(fields, input())
);
submitEvent.respondWith?.(result);
return;
}
void submitRegistration(
registrationRequest,
input(),
{ mode: "human" }
);
});
Agent 分支回傳:
{
"ok": false,
"code": "CONFIRMATION_REQUIRED",
"retryable": false,
"eventId": "evt-webmcp-intro"
}
這裡的 ok: false 是 AgentReady Events 的專案語意:最終業務 mutation 尚未完成,不代表
prepare handler 發生錯誤。真正的狀態由 CONFIRMATION_REQUIRED 說明,且這個分支不呼叫/api/registrations。
Browser test 也會直接計算 POST 次數:Agent prepare 後必須是 0;只有人類按下確認後才會
變成 1。
修正版部署為 revision 0000008 後,我用相同 Prompt 執行 Attempt 3:

圖 4:表單已填入示範資料,Network 沒有 registration POST,Inspector 收到 CONFIRMATION_REQUIRED。
這次 trace 只有一次 prepare_event_registration,姓名與 Email 都和 Prompt 相同;function
response 回來後,Gemini 也明確告訴我資料已準備完成,必須自己檢查並手動送出。
Attempt 3 因此通過 prepare-only contract。它沒有證明 Agent 可以建立正式報名,也沒有把
Attempt 2 的 completion failure 改寫成成功。
Client 傳入:
{ "interactionMode": "human" }
不能證明真人真的按了按鈕。同源程式一樣可以偽造這個字串,所以它只能是 audit metadata,
不是 authorization。
正式 POST 前,client 會先取得一個短效、單次使用且綁定 session/action/target 的
confirmation intent。Server 還要重新驗證 CSRF、活動狀態、名額與 intent;憑證一旦用過、
過期或換到另一個 event ID,都會被拒絕。
這個 gate 能保護本 Demo 的受控 Journey,但我不會把它寫成「server 可以從封包辨認真人
手指」。付款或其他高風險系統仍可能需要重新驗證、WebAuthn 或更完整的交易簽章。
人類正式送出後,server-side inventory 才從 8 變成 7。第一次取消會補回 8,重複取消不再
增加;兩個 session 搶最後一席時,也只能有一個成功。這些規則由 server service 與 tests
把關,不靠 Tool description 自我感覺良好。
這一天不是從 FAIL 走到「Agent 會報名」,而是把一支 prepare Tool 的生命週期補完整:
自然語言
→ 正確 Tool 與 input
→ 可見表單
→ CONFIRMATION_REQUIRED
→ registration POST = 0
→ Agent 請人類自行確認
Attempt 3 是 revision 0000008 的單次 Agent 證據;browser 與 server tests 補上 deterministic
邊界。兩者放在一起,才把「看起來停住」提升成「Tool completion 完成,副作用仍被擋住」。
明天把同一個 prepare-only 原則搬到取消流程。不過取消還多了幾個麻煩:一句「這個我不要了」
到底指哪一筆?那筆是不是目前 session 的?重複取消又會不會把名額補回兩次?