承認とフォーム
自分の UI から Tool 呼び出しを承認し、フォームに回答します。
返金の実行、メールの送信、顧客が料金を支払う 2 つのプランからの選択など、モデルに任せるべきではない判断があります。Subako では、Agent が人の判断を待つ方法が 2 つあります。どちらも Session のログ上の 1 組の Event なので、人がリアルタイムで見ていても、1 時間後に Session を開いても同じように動作します。
| 承認 | フォーム | |
|---|---|---|
| 求める側 | Policy が 承認を求める の Tool | バージョンで 人への質問 が許可された Agent 自身 |
| 人が決めること | この呼び出しを実行するかどうか | 最大 4 つの質問への回答 |
| Event | approval_requested → approval_resolved |
input_requested → input_resolved |
| 確定に使うルート | POST /v1/sessions/{id}/approvals/{call_id} |
POST /v1/sessions/{id}/inputs/{call_id} |
| 24 時間誰も応答しないとき | 「approval timed out」として拒否されます | 期限切れになり、誰も回答しなかったことがモデルに伝わります |
どちらのルートも、Session token か、Session › 管理 の権限を持つ API key で呼び出せます。コンソール も同じルートで確定しています。
承認
承認が必要な呼び出しに差しかかると、Run は approval_requested をログに追記して待機します:
| フィールド | |
|---|---|
call_id |
判断の対象となる呼び出し |
server |
呼び出しが属する Grant (mcp:orders、fn:billing、sandbox:build など) |
tool |
Tool の名前 |
arguments |
モデルが呼び出しに渡そうとしている引数 |
args_hash |
呼び出しが再度行われたときに照合に使う値 |
arguments は加工せずにそのまま人に見せてください。人が実際の呼び出しを確認できることに、承認の意味があります。そのうえで確定します:
curl -X POST https://api.us.cloud.subako.ai/v1/sessions/$SESSION_ID/approvals/$CALL_ID \
-H "Authorization: Bearer $SUBAKO_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"decision": "deny", "message": "Refunds over $500 need a manager."}'
allow の場合は呼び出しが実行され、続いてその tool_result が届きます。deny の場合は拒否されたことが (message を指定していればその内容とともに) モデルに伝わり、Run は続きます。message を指定できるのは deny のときだけです。allow と一緒に送るとリクエストは拒否されるので、モデルに届かないテキストを承認に添えてしまうことはありません。
レスポンスは approval_resolved Event です。保留中の承認がない呼び出しには 404、すでに確定した呼び出しには 409 が返ります。
拒否はエラーではなく通常の結果なので、メッセージを書いておく価値があります。「承認されませんでした」だけではモデルは理由を推測するしかありませんが、「$500 を超える返金にはマネージャーの承認が必要です」とあれば、次に何をすべきかがわかります。
フォーム
人への質問 が許可された Agent のバージョン (Agent config の builtin.ask_user) では、モデルにフォームを出すための Tool が渡されます。モデルがフォームを出すと、Run は input_requested をログに追記します:
{
"type": "input_requested",
"call_id": "call_8f2a",
"request": {
"questions": [
{
"id": "day",
"prompt": "Which day works for you?",
"required": true,
"widget": { "type": "select", "data": { "options": [{ "label": "Tue" }, { "label": "Wed" }], "multiple": false, "allow_other": false } }
},
{ "id": "seating", "prompt": "Any seating preference?", "required": false, "widget": { "type": "text", "data": { "multiline": false } } }
]
}
}
| Widget | 表示するもの | 回答 |
|---|---|---|
select |
1 つ、または複数 (multiple) を選ぶ選択肢。自由入力の その他 (allow_other) を付けることもできます |
選んだラベルの配列 |
text |
1 行、または複数行 (multiline) のテキスト欄 |
文字列 |
confirm |
はい・いいえ | 真偽値 |
custom |
アプリ独自の Widget。id で指定し、Agent が値を入れた props が付きます |
answer のスキーマに従った値 |
すべての質問にそれぞれ 1 回ずつ回答します:
curl -X POST https://api.us.cloud.subako.ai/v1/sessions/$SESSION_ID/inputs/$CALL_ID \
-H "Authorization: Bearer $SUBAKO_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "submitted",
"data": {
"answers": [
{ "question_id": "day", "outcome": { "type": "answered", "data": { "value": ["Tue"] } } },
{ "question_id": "seating", "outcome": { "type": "skipped" } }
]
}
}'
answeredには回答の値を入れます。skippedは必須でない質問を飛ばすときに使います。unsupportedは、知らない Custom widget など、その Widget をクライアントで表示できないことを伝えます。どの質問にも使え、モデルには別の方法で質問するよう伝わります。{"type": "declined", "data": {"message": "Not now."}}はフォーム全体を閉じます。人が回答しないことを選んだことが、添えたメッセージとともにモデルに伝わります。
レスポンスは input_resolved Event です。質問に合わない回答は理由を添えて拒否され、フォームは開いたままになります。

Custom widget
Custom widget は Agent のバージョンで宣言します。Agent が入れる値と回答の内容は、それぞれスキーマで定めます:
{
"builtin": {
"ask_user": {
"widgets": [
{ "type": "select" },
{ "type": "confirm" },
{
"type": "custom",
"id": "seat_picker",
"description": "Pick seats on the venue map.",
"props": { "type": "object", "properties": { "venue": { "type": "string" } } },
"answer": { "type": "array", "items": { "type": "string" } }
}
]
}
}
}
モデルは description を読んで Widget を選び、props に値を入れます。アプリはその Widget を表示し、answer に合う値で回答します。