メインコンテンツへスキップ

GPT-6 Astra APIチュートリアル:非同期ツールとステアリングでリリースチェックエージェントを構築

OpenAI API経由でGPT-6 Astraを用い、非同期ツール、推論制御、構造化出力、コスト追跡を備えたPython製リリースチェックエージェントを構築し、コンピュータ利用とステアリングを検証します。
更新 2026年9月7日  · 14 分 読む

AIで探索

ChatGPTClaudePerplexity

最初にGPT-6 Astraに同じターン内で遅いツールと速いツールを渡したとき、私は従来の同期ループを想定していました。つまり、遅いツールを要求して、完了を待つ間はコードがブロックされるだろうと。 OpenAIの非同期ツール呼び出しのドキュメントでは、代わりにAstraが処理を続行できると説明していましたが、半信半疑でした。アプリケーション側がバックグラウンド処理を依然として管理するため、非同期ツール呼び出しはオーケストレーション自体を取り除くわけではありません。問題は、それでも十分に状況が変わるかどうかです。

当社のGPT-6 Astra概要ではリリースとベンチマークを、GPT-6 Astra vs. Claude Fable 5.1ガイドでは、最大の競合との性能と価格を比較しています。本チュートリアルでは、テストスイート、ヘルスエンドポイント、固定ブラウザチェックを備えたリリースチェックのワークフローをGPT-6 Astraで構築します。別デモとして、モデル作成のコンピュータ利用とターン途中のステアリングも扱います。

以下を取り上げます。

  • GPT-6 Astra APIの呼び出し方
  • ベースラインとして同期ツール呼び出しループを構築
  • 遅いチェックを非同期ツール呼び出しに切り替え
  • 制限付きのコンピュータ利用チェックを実行
  • WebSocket経由でのステアリングを、完了後に再開するベースラインと比較
  • 最終診断時のみ推論努力を引き上げる
  • 構造化出力で検証済みのgo/no-goレポートを返す
  • キャッシュ書き込みを含め、APIコストを正確に計算
  • Streamlitで実行をライブ視聴
  • asyncジョブが生むエッジケースへの対応

要約(TL;DR)

GPT-6 Astraは標準のResponsesループに、非同期ツール呼び出し、WebSocketによるターン途中のステアリング、会話途中での推論努力の変更という3つのAPI機能を追加します。これら3つを単一のリリースチェックエージェントに統合したところ、設計方針を変える4つの発見がありました。

  • 非同期ツール呼び出しは待ち時間を短縮したが、モデルの作業量は変わらない:ターン数は依然としてモデルの呼び出し順序に依存。
  • ステアリングは、完了→再開のベースラインより短時間だったが、すべての再開ポリシーを比較したわけではない。
  • 推論努力を上げても診断が必ずしも変わるわけではない。推論トークンは増えるのに。
  • チェックを同時に走らせるとレースコンディションが露出し、逐次版では見えない場合がある。

これらの結果は今回のリリースチェックに関するもので、すべてのエージェント業務に当てはまるとは限りません。ツールの所要時間、共有状態、モデルが要するターン数によって結果は変わり得ます。

GPT-6 Astra APIとは?

GPT-6 Astra APIは、2026年9月3日にリリースされたOpenAIの新たなフラッグシップモデルへ、Responses API経由でアクセスする方法です。本チュートリアルで重要なのはインターフェースです。gpt-6-astra はOpenAIのResponses APIでテキストと画像入力を受け付けます。推論努力は lowからmaxまでで、none はありません。

本チュートリアルの例では client.responses.create を使用し、client.chat.completions.create は使いません。移行時はリクエスト、出力、ツール結果の形式も変更が必要です。さらに temperaturetop_p、ログ確率の設定はAstraが非対応のため削除してください。最初の呼び出しの前に、価格を確認しておきましょう。

GPT-6 Astraの料金はいくら?

入力トークンが最大272,000までのリクエストでは、標準料金は通常の入力トークン100万あたり$10、出力トークン100万あたり$50です。キャッシュ済み入力は100万あたり$1、キャッシュ書き込みは100万あたり$12.50です。

しきい値を超えると、OpenAIは入力とキャッシュ関連に2倍、出力に1.5倍の係数を適用します。高いレートは超過分だけでなく、リクエスト全体に適用されます。本チュートリアルの実行はいずれもしきい値に達しませんでした。

GPT-6 Astra APIで何を作る?

ステージングアプリは小さなFlask製のタスクボードです:ホームページ、タスク追加フォーム、完了ボタン、そして /health エンドポイント。完全なコード(ステージングアプリを含む)はGitHubリポジトリにあります。

エージェントがチェックするステージングのタスクボード。タスクリストと追加フォームが表示されている

テスト前に3件のシード済みタスクが表示されます。画像:著者作成。

アプリには意図的な不具合が1つあります。エージェントには3つのチェックがありますが、それらを検出できるよう設計されているのはテストスイートだけです。

なぜアプリは空のタスクタイトルを受け入れるのか?

タスク作成エンドポイントは空タイトルを拒否しません。プロンプトで明かさずに、既知の失敗をエージェントが見つけられるよう、この挙動を残してあります。

エージェントはどのリリースチェックを実行できる?

エージェントは3つのツールを呼び出せます:

  • run_test_suitepytestを実行し、約250件のHTTPリクエストを含む一括インポートテストも含みます。

  • check_ui_flowPlaywrightでタスクを追加し、表示を確認します。

  • check_staging_health/health にGETリクエストを送信します。

いずれもモックなしでステージングを相手に実行します。ブラウザチェックは固定コードで、モデル作成のコンピュータ利用デモは後半で扱います。

PythonでGPT-6 Astra APIをセットアップする方法

gpt-6-astraにアクセス可能なOpenAI APIキーが必要です。platform.openai.com/api-keysでキーを作成し、プロジェクトでgpt-6-astra が有効になっていることを確認してください。エンタープライズワークスペースでは、リリース時点でAstraがデフォルト無効です。

以下のコマンドはWindows PowerShellを使用し、本チュートリアルに必要なパッケージをすべてインストールします。ステアリングのデモ用にOpenAIのrealtimeも含みます。

python -m venv .venv
.venv\Scripts\Activate.ps1
Copy-Item .env.example .env
pip install "openai[realtime]" flask pytest playwright pydantic matplotlib python-dotenv streamlit
playwright install chromium

macOSやLinuxでは、アクティベーションをsource .venv/bin/activate 、コピーをcp .env.example .envに置き換えてください。

次にAPIキーを新しい.envに追加します。コピーした.env を開き、OPENAI_API_KEY=sk-...を追記します。これはpython-dotenvが読み込み、SDKが自動的に認識するため、コード内でキーを渡す必要はありません。

プロジェクト固有の依存や.envファイルに不慣れな場合は、当社の仮想環境および環境変数ガイドをご覧ください。キーが機能することを確認してから先へ進んでください。

すでにResponses APIでAPIキーが動いている場合は、次の小節を飛ばし、同期ツールループから始めてください。最初のリクエストはセットアップ確認のみです。

最初のGPT-6 Astra API呼び出しを行う

キーを用意したら、dotenvで読み込みます。その後、OpenAIクライアントを作成し、client.responses.create()で最初のリクエストを送信できます。最小のリクエストは次のとおりです。

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI()
response = client.responses.create(
    model="gpt-6-astra",
    reasoning={"effort": "low"},
    input="In one sentence, what is a staging environment used for?",
)
print(response.output_text)

レスポンスのusage フィールドには、後ほどコスト計算に必要なトークン数が記載されています。

同期のGPT-6 Astraツールループを構築する

以下のスニペットは抜粋です。実行可能な版は付随のGitHubリポジトリにあります。

まず非同期には手を付けず、通常版を構築しました:

  1. モデルを呼び出す

  2. function_call項目の有無を確認

  3. 該当ツールを実行

  4. previous_response_idで結果を送り返す

  5. モデルがツール要求をやめるまで繰り返す

このブロッキングループが、非同期比較のベースラインです。

for turn in range(max_turns):
    response = client.responses.create(
        model="gpt-6-astra",
        reasoning={"effort": "low"},
        tools=TOOLS,
        input=next_input,
        previous_response_id=previous_response_id,
    )
    calls = [item for item in response.output if item.type == "function_call"]
    if not calls:
        final_text = response.output_text
        break
    outputs = [
        {"type": "function_call_output", "call_id": call.call_id, "output": run_tool(call)}
        for call in calls
    ]
    next_input, previous_response_id = outputs, response.id

ベースライン実行での発見

ベースライン実行では、モデルは各ツールを順に呼び出しました。既知のバリデーション失敗を検出し、no-goの判断を返しました。3回の実行で、総ウォールクロック時間は平均23.40秒でした。この平均はツール個別ではなく、全体の実行時間です。

GPT-6 Astraの非同期ツール呼び出しはどう動く?

ツールのスキーマに"async": true を付けると、アプリはその結果を後回しにでき、その間にモデルは作業を続行するか待機します。ツールの実行とバックグラウンドジョブの管理は引き続きアプリ側の責務です。

独立したチェックを同時実行する方法

私は run_test_suitecheck_ui_flow を非同期にし、このデモでは保留中の作業バッチが常に1つのみであるため、引数なしのアプリ定義wait_for_tasks ツールを追加しました。引数なしのwaitにより、例を小さく保てます。

本番ランナーでは、ジョブをタスクハンドルで識別し、各ハンドルを元のcall_id に紐づけ、次のステップが保留中の結果に依存するときのみ待機します。

各完了済み結果は元の call_id に対して返し、その後にwaitツール自身のcall_id で待機状態を返します。

TOOLS = [
    {"type": "function", "name": "run_test_suite", "async": True, ...},
    {"type": "function", "name": "check_ui_flow", "async": True, ...},
    {"type": "function", "name": "check_staging_health", ...},
    {"type": "function", "name": "wait_for_tasks", ...},
]

この実行では、アプリは各マーク済み呼び出しをスレッドプールに投入しました。バックグラウンドチェックを実行している間に、速い同期ヘルスチェックを実施し、その後wait_for_tasks で保留中チェックの完了までブロックしました。

非同期ツール呼び出しでウォールクロック時間は短縮する?

3回の実行で、非同期は平均ウォールクロック時間を23.40秒から18.94秒へと19.1%短縮しました。平均だけ見ると単純に見えますが、実行ごとのばらつきは大きく、以下のグラフのとおりです。3回では本事例を示すには十分ですが、本番レイテンシを予測するには不十分です。

同一リリースチェックの同期3回と非同期3回のウォールクロック秒を比較した折れ線グラフ

両モードで実行時間にばらつきがありました。画像:著者作成。

なぜ並行チェックでレースコンディションが起きた?

ブラウザチェックと一括インポートテストを同時に実行すると、双方が同じインメモリのタスクリストを変更するため、一括インポートのアサーションが失敗することがありました。テストが排他的アクセスを前提にしていたためです。ランナーかテストのどちらかでデータを分離すれば、レースは回避できます。

UIテスト向けの制限付きGPT-6 Astraコンピュータ利用

コンピュータ利用について、GPT-6 Astraのドキュメントはコード実行を推奨し、代替として構造化されたcomputer ツールも引き続きサポートしています。コード実行では1回の呼び出しで複数の操作、ループ、条件分岐を組み合わせられます。一方、computer ツールは構造化されたマウスやキーボード操作を1件ずつ返し、アプリ側で翻訳・再生します。

コンピュータ利用ランナーを制限する方法

クラス名をBrowserSandboxにしましたが、名前ほど強固ではありません。モデル作成のコードにPlaywrightのpagelog() 関数、expect_text() ヘルパーを与えます。Pythonは exec() に組み込みを注入でき、ページは他オリジンにも移動可能です。

sandbox_globals = {"page": self.page, "log": log, "expect_text": expect_text}
exec(code, sandbox_globals)

これはセキュリティ境界ではなく、あくまでデモ用ランナーとして扱ってください。モデル作成のコードには、ファイルシステム、プロセス、ネットワーク、オリジンに制限を設けた分離プロセスまたはコンテナが必要です。

UIチェックでは何が起きた?

境界を設けるにはハードストップが必要なため、ループを8ターンに制限しました。モデルはページを確認し、フォーム入力を見つけ、「UI flow check, cobalt otter 73921」というタイトルを作成・送信し、表示を確認しました。所要は13秒でした。詳細例は、Astraの前身モデルを使ったGPT-5.4のコンピュータ利用チュートリアルをご覧ください。

GPT-6 Astraのターン途中ステアリングはどう動く?

ターン途中のステアリングは、gpt-6-astra でのみ利用可能で、WebSocket接続が必要です。

接続を開いてレスポンスを作成し、生成中に新しい指示を含むresponse.steer イベントを送信します。response.steer.accepted は、更新が適用待ちであることを意味します。自動継続を作成する前に、サーバーは現在の出力アイテムと、すでに走っているホスト側ツール作業を完了させます。

クライアントのツール結果や承認がまだ必要な場合は、response.steer.pending が不足している入力を示します。

async with client.responses.connect() as connection:
    await connection.response.create(model="gpt-6-astra", input=TASK)
    async for event in connection:
        if event.type == "response.created" and initial_response_id is None:
            initial_response_id = event.response.id
            await asyncio.sleep(1.0)
            await connection.response.steer(
                previous_response_id=initial_response_id,
                input="Also add a rollback plan, but skip mobile.",
            )

1秒の遅延は実験コードに合わせ、最初のレスポンスの開始時間を確保するためのものです。これがないと、レスポンスライフサイクルの別段階をテストすることになります。

ターン途中ステアリングで変わらないものは?

ステアリングは元のレスポンスを書き換えません。更新で中断された場合、そのレスポンスはstatus: "incomplete"incomplete_details.reason: "steered" で終了します。続いて、後継のレスポンスが新しい指示で継続します。

最初のレスポンスが更新適用前に完了した場合は、そのまま完了のままです。ステアリングは、すでに開始されたクライアント側のアクションを元に戻したりキャンセルしたりしません。そこの制御はアプリケーション側にあります。キューイングされたステアリングは現在のWebSocket接続にのみ存在するため、再接続前に更新内容を記録してください。

比較パスでは、最初のレスポンスを完了させた後、結合した指示で新しいリクエストを送信します。2回の実行で、ステアリングは平均26.91秒、完了→再開パスは50.02秒でした。この比較は、キャンセル→再開ポリシーを含まず、最終テキストの正確性も評価していません。

ステアリング対再開での平均秒数と平均コストを比較した棒グラフ2種

時間とコストの2回平均。画像:著者作成。

再開パスは同様の要求を含む2つの完全なレスポンスを生成します。その構成が時間差の一因であり、この2回の結果をステアリングの一般的なベンチマークとみなすべきではありません。

会話途中でGPT-6 Astraの推論努力を変更する方法

gpt-6-astra low から max までの推論努力をサポートします。努力を上げると推論トークンの使用は増え得ますが、必ず異なる答えになるとは限りません。configuration_update は、次回以降のレスポンスを変更し、新たな更新で上書きされるまで有効です。リクエストレベルの設定自体は変わりません。

このパイプラインではレポートが会話を終えるため、更新は最終ステップのみに影響します。

response = client.responses.create(
    model="gpt-6-astra",
    previous_response_id=previous_id,
    reasoning={"effort": "low"},  # default remains low
    input=[
        {"type": "configuration_update", "reasoning": {"effort": "high"}},
        {"role": "user", "content": "Diagnose the root cause and recommend a fix."},
    ],
)

同じ失敗トレースを両方の努力レベルで使い、診断とトークン使用を比較しました。

注意点が1つ:レスポンスのreasoning.effort フィールドは、configuration_update で選ばれた努力ではなく、あくまでリクエストレベルの設定を報告します。適用確認にこのフィールドを使わないでください。

高い推論努力で診断は変わった?

統合実行全体では、引き上げたステップで108個の推論トークンを使用しました。その他のlow のステップはゼロでした。より長い失敗トレースを使った以前の単独テストでも、lowはゼロ、高は318でした。どちらもバリデーションバグを特定できたため、努力を上げるとトークン使用は変わるが、診断は変わらない場合があると言えます。

構成更新は標準の単一エージェントリクエストでのみ機能します。更新を連続させるとAPIは拒否し、自動圧縮や自動切り詰めと併用はできません。

GPT-6 Astraの構造化出力を使う方法

パイプラインの最終ステップでは、client.responses.parseを呼び出して、自由記述テキストを検証済みのPydanticモデルに置き換えます。

class GoNoGoReport(BaseModel):
    decision: str
    summary: str
    checks_completed: list[str]
    failures: list[str]
    risks: list[str]
    follow_up_actions: list[str]
    confidence: float

response = client.responses.parse(
    model="gpt-6-astra",
    previous_response_id=previous_id,
    text_format=GoNoGoReport,
    input=[...],
)

Pydanticはここで宣言したフィールド型を検証します。レポートが証拠と一致することまでは保証せず、この版ではdecision を2値に、confidence を範囲に制限していません。

go/no-goレポートは何を捉えた?

レポートはdecision: "no_go"を返し、前述のバリデーション失敗をfailuresに、並行性の問題をrisksに分類しました。後者については「共有ステージングデータに対する同時UIチェックからの干渉の可能性」と記載しました。プロンプトはそのリスク名を挙げていません。

レイテンシとコストはこのスキーマの外で扱い、コードで計算してください。モデルはツールからの証拠をもとにレポートのフィールドを埋めます。

Streamlitインターフェースはコマンドラインランナーと同じジェネレーターを利用します。ツールイベントを到着順にレンダリングし、その後、解析済みレポートをReport タブとJSON タブに表示します。

ライブのエージェント進行状況と最終レポートの並行表示。動画:著者作成。

ダッシュボードは、固定ブラウザチェック、推論更新、構造化レポート、タイミング表示を含む非同期のリリースパイプラインを統合実行します。コストパネルはコマンドラインランナーと同じ元帳を読みます。モデル作成のコンピュータ利用とステアリングの個別デモは実行しません。

GPT-6 Astra APIのトークン使用量とコストを追跡する方法

これらの実行におけるモデルトークン部分について、usage フィールドには、各レスポンスの価格算定に必要な4種のトークン数が含まれます。キャッシュ書き込みは独自レートのため、通常の入力に含めないでください。ここでのツールはアプリ側で実行されます。別料金のホストツールを追加する場合は、その費用も加算しましょう。

details = usage.input_tokens_details
cached_tokens = details.cached_tokens
cache_write_tokens = details.cache_write_tokens
ordinary_tokens = usage.input_tokens - cached_tokens - cache_write_tokens

cost = (
    ordinary_tokens * PRICE_INPUT
    + cached_tokens * PRICE_CACHED_INPUT
    + cache_write_tokens * PRICE_CACHE_WRITE
    + usage.output_tokens * PRICE_OUTPUT
) / 1_000_000

この計算は1レスポンス分です。元帳は各レスポンス後に適用し、呼び出し単位で合算します。

cache_write_tokens は値がゼロでも記録してください。そうしないと、将来のキャッシュ書き込みが通常の入力数に紛れ込み、課金ミスに気づく厄介な原因になります。

GPT-6 Astraエージェントの本番運用に向けた考慮事項

このデモをデプロイのゲートにする前に、以下の変更が必要です。

ツールの権限と分離

ステージング境界を維持し、上記のとおりBrowserSandboxを分離してください。データベース認証情報や本番アクセスは付与しないでください。

非同期ジョブのライフサイクル

このデモでは、すべての保留ジョブが完了し、他からの介入もありません。本番ランナーは、いずれも成り立たない状況を生き延びる必要があります。各保留エントリに対し、次が必要です:

  • 期限と最終状態(無期限の保留を防ぐ)
  • 配信フラグ(コールバックとリトライが同じ結果を二重送信しないように)
  • 開始・終了のタイムスタンプ(タイムアウトと、遅延したが有効な完了の区別に)
  • 重複呼び出しの拒否(同一タスクが二重起動されないように)
  • バックグラウンドスレッドのエラーハンドリング(例外がプール内で消えないように)
  • キャンセル(遅延結果を無視し、すでに進行中の外部作業を停止する)

ステアリングと不可逆アクション

ステアは将来の指示を変更できますが、完了済みの副作用は元に戻せません。ツールがすでに外部システムを変更している場合は、別のツールアクションで補償する必要があります。

ミスアラインメント監視

自動停止は、永続化推論、WebSocket、またはOpenAIの圧縮を使用するResponses APIリクエストに適用されます。その他のリクエストはアラートこそ出ますが、自動停止はしません。

ストリーミング前には、ミスアラインメント監視により、HTTP 403とコードmisalignment_policy_violationで実行がブロックされることがあります。ストリーミングクライアントでは、出力開始後にエラーを受ける場合があります。停止された会話を一般に再開する経路はAPIにありません。

GPT-6 Astraエージェントのデプロイチェックリスト

このエージェントをローカルデモからデプロイへ進める前に、次の管理機構を追加してください。これらはモデル指示ではなく、アプリケーションコード側に実装します。

  • ステージングアプリのHTTP呼び出しとWebSocket接続に明示的なタイムアウトを設定

  • 通常入力、キャッシュ入力、キャッシュ書き込み、出力、レスポンスID、ターン数をログに記録

  • 不完了の実行、ターン上限到達を検知してアラート。予算超過のアラートは別途設定

  • openai SDKのバージョンを固定し、アップグレード前に非同期・ステアリング・configuration_updateの挙動を再確認

GPT-6 Astraの非同期ツールやステアリングはいつ使うべき?

要件に合う中で最も単純な方法を選んでください。

  • ツールの返りが速く、要件が固定なら、同期リクエストと構造化出力から始める。
  • 遅い呼び出しの間にモデルや別ツールが有益な作業を進められ、そのためのジョブ管理の増加に見合うなら、非同期ツール呼び出しを追加する。
  • 実行中に要件が変わるなら、ターン途中のステアリングを使う。

まとめ

遅いツールを重ね合わせられるようになって、同期のリリースチェックはより有用になりましたが、きれいな完勝ではありませんでした。3回の実行で、非同期は平均時間を23.40秒から18.94秒に短縮する一方、逐次ループでは隠れていた共有状態のレースを露出させました。

私はブラウザとテストデータを分離し、通常ターンはlowに保ち、証拠の精査が必要なときだけ推論努力を上げます。ツールが素早く終わり、要件が固定なら、構造化出力付きの同期ループで十分です。独立作業が重なり得るなら非同期を、レスポンス中に指示が変わるならステアリングを使いましょう。

APIの基礎は、当社のd Working with the OpenAI API コースをおすすめします。より大規模なエージェントシステムについては、Building Scalable Agentic Systems コースをご覧ください。

FAQs

GPT-6 AstraでChat Completionsは使えますか?

プレーンテキストについては可能です。ツール呼び出しについては不可です。AstraはResponses APIが必須のため、本稿の例はすべてclient.responses.createを使用しています。

非同期ツール呼び出しとステアリングをサポートするモデルは?

非同期ツール呼び出しはGPT-6 Astraで導入されました。ターン途中のステアリングはAstra専用かつWebSocket専用で、GPT-5.6以前は非対応です。

既存リクエストをgpt-6-astraに切り替えると何が壊れますか?

3点あります。reasoning.effort: "none"はHTTP 400になるのでlowから始めてください。temperaturetop_p、ログ確率の設定は削除が必要です。そしてツール呼び出しは、まだならResponsesへ移行が必要です。

非同期ツール呼び出しは並列ツール呼び出しの代わりになりますか?

いいえ。目的が異なります。並列ツール呼び出しは1ターンで複数のツールをモデルに要求させる機能で、非同期はアプリが特定のツール結果を後回しにしつつモデルを先に進ませる機能です。

なぜ非同期ツール呼び出しでmissing function_call_outputエラーが出た?

同一バッチ内に未解決の非同期でないツール呼び出しがあると発生することがあります。非同期はマークした呼び出しだけを後回しにでき、他のツール呼び出しは引き続き先に出力が必要です。

非同期ツール呼び出しにWebSocketは必須ですか?

不要です。上記の非同期実装は通常のResponses API呼び出しを使っています。

空タイトルのバグは、テストスイートではなくUIチェックで捕捉されることはありましたか?

いいえ。空タイトルの検証を行ったのはテストスイートだけで、UIとヘルスチェックは別の挙動をテストしました。

ツールループでprevious_response_idを使うのはなぜ?

各ツール結果を、それを要求したレスポンスに結び付けます。これにより、毎回全トランスクリプトを再送せずに、同じResponses APIの会話を継続できます。

トピック
人工知能
大規模言語モデル
AIエージェント

DataCampでAIを学ぼう!

Tracks

開発者向けアソシエイトAIエンジニア

26時間
APIやオープンソースライブラリを使って、ソフトウェアアプリケーションにAIを統合する方法を学びます。 AIエンジニアになるための旅を今日始めましょう!
詳細を見るRight Arrow
コースを開始
もっと見るRight Arrow