---
title: 承認とフォーム
description: 自分の 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 で呼び出せます。[コンソール](/ja/console/sessions#tool-の呼び出しを承認する) も同じルートで確定しています。

## 承認

承認が必要な呼び出しに差しかかると、Run は `approval_requested` をログに追記して待機します:

| フィールド | |
| --- | --- |
| `call_id` | 判断の対象となる呼び出し |
| `server` | 呼び出しが属する Grant (`mcp:orders`、`fn:billing`、`sandbox:build` など) |
| `tool` | Tool の名前 |
| `arguments` | モデルが呼び出しに渡そうとしている引数 |
| `args_hash` | 呼び出しが再度行われたときに照合に使う値 |

`arguments` は加工せずにそのまま人に見せてください。人が実際の呼び出しを確認できることに、承認の意味があります。そのうえで確定します:

```bash
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](/ja/reference/agent-config#builtin) の `builtin.ask_user`) では、モデルにフォームを出すための Tool が渡されます。モデルがフォームを出すと、Run は `input_requested` をログに追記します:

```json
{
  "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 回ずつ回答します:

```bash
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 です。質問に合わない回答は理由を添えて拒否され、フォームは開いたままになります。

![コンソールで入力欄の上に表示されたフォーム](/blume-assets/content/docs/_images/ja/session-form.webp)

### Custom widget

Custom widget は Agent のバージョンで宣言します。Agent が入れる値と回答の内容は、それぞれスキーマで定めます:

```json
{
  "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` に合う値で回答します。
