publishPost が approval 待ちに入った。ところが画面は spinner のまま。ユーザーには「処理中」なのか「確認待ち」なのか分からない。
この状態で model を替えても、運用はあまり良くならない。approval 待ち、timeout、tool error は、次に取るべき操作がそれぞれ違うからだ。ひとつの isLoading に押し込むと、agent が賢くなるほど UI の説明が雑になる。
AI SDK 7 には tool 単位の approval、複数粒度の timeout、OpenTelemetry 連携が入った。全部を紹介するより、まず frontend と backend が共有する runtime contract を小さく決めたい。
side-effect contract : どの tool を人間が止めるか
time contract : どこで何秒待つか
trace contract : どの run / session を追うか
前提は Node.js 22 以上と ESM。ここからは、小さな social card 公開 agent を例にする。
approval_required は error ではない
tool を先に、副作用で分ける。
type ToolPolicy = {
effect: 'read' | 'external'
approval: 'none' | 'user'
}
const toolPolicies = {
findUiReferences: { effect: 'read', approval: 'none' },
publishPost: { effect: 'external', approval: 'user' },
} satisfies Record<string, ToolPolicy>
ToolLoopAgent などで tool ごとの approval policy を設定できるので、止めるのは publishPost だけでいい。read-only の検索まで毎回確認させると、安全性より先に確認疲れが来る。
画面側では、少なくとも state を分けておく。
type AgentRunState =
| 'queued'
| 'running'
| 'approval_required'
| 'resumed'
| 'succeeded'
| 'timed_out'
| 'failed'
approval_required では tool 名、input の要約、approve / deny を表示する。deny は通信失敗ではない。ユーザーが選んだ正常な分岐だ。
再開時には、最初に見せた input と現在の input が同じか、policy が変わっていないかを再検証する。AI SDK 7 は approval の再開時に input と policy を検証でき、高リスクな操作では HMAC で input と approval token を結び付けられる。ただし approval は「この side effect を実行してよい」という判断材料であって、投稿内容の正しさや rollback まで保証する仕組みではない。
timeout を一個の数字にしない
timeout: 60_000 だけでは、どこが止まったのか分からない。run 全体、model の一段、stream の沈黙、tool 実行では、失敗後の扱いが違う。
以下は AI SDK の schema ではなく、team が repo で共有する policy の例だ。
const runtimePolicy = {
timeoutMs: {
total: 60_000,
step: 10_000,
chunk: 2_000,
defaultTool: 5_000,
tools: {
publishPost: 8_000,
},
},
} as const
-
total: run が際限なく続くのを止める -
step: model call や orchestration の一段を区切る -
chunk: stream が黙ったままになるのを検出する -
tool: 外部処理ごとの待ち時間を決める
AI SDK 7 では total、step、chunk、default tool、per-tool の timeout を持てる。TimeoutError と abort reason も stream / UI protocol に流せるので、画面で timed_out を普通の failed と分けられる。
timeout したからといって、外部処理が巻き戻るわけではない。publishPost が外部 API に届いた直後に client 側が timeout した可能性は残る。再試行できるかどうかは、tool の idempotency key や外部サービスの状態確認まで含めて決める必要がある。
runId と sessionId を画面にも出す
telemetry を入れても、画面のエラーと trace がつながらなければ調査は遅い。model 名より先に、run と session を共通キーにする。
type AgentRunEvent = {
runId: string
sessionId: string
step: number
tool?: string
state:
| 'running'
| 'approval_required'
| 'resumed'
| 'succeeded'
| 'timed_out'
| 'failed'
elapsedMs: number
finishReason?: string
}
AI SDK 7 は registerTelemetry と @ai-sdk/otel で model call、step、tool、agent execution を OpenTelemetry へ接続できる。lifecycle callback からは callId、step number、finish reason、usage、error、performance を拾える。
Vercel Sandbox では CPU、memory、data transfer などを Sandbox Session ID 単位で group 化できる。これは特定 runtime の例だが、「どの model を使ったか」だけでなく「どの session が resource を使ったか」を追う設計の分かりやすい参考になる。
context を丸ごと attribute に入れるのはまずい。prompt、token、投稿先 credential のような値は、必要な項目だけ明示的に telemetry へ渡す。
social card agent に失敗を注入する
success path を一度眺めるより、復旧方法の違う失敗を先に起こしたほうが contract の穴を見つけやすい。
今回の workflow 境界はこうする。agent tool ではない手動工程も、同じ図に入れておく。
findUiReferences read-only approval 不要
prepareImage browser-local manual approval 不要
publishPost external side effect user approval 必須
findUiReferences は UI パターンを探すだけの read-only step にする。MCP Apps UI、A2UI、AG-UI の資料を横断するときは、生成AI UIデザインのリソース集のような索引を人が参照する形でも足りる。agent integration が必要な処理ではない。
画像も同じだ。prepareImage という名前だからといって、pixel を agent backend に送る必要はない。Resize Image Forのように browser 内で resize と export を完結できる道具を手動工程に置けば、画像データを外へ送る tool 自体を増やさずに済む。
次の順で壊してみる。
-
publishPostを approval 待ちにして、UI がapproval_requiredへ移るか確認する。 - deny を選び、
failedやtimed_outと別の結果として記録できるか見る。 - tool を意図的に遅らせ、tool timeout と total timeout を別 event にできるか確かめる。
- tool error を返し、画面の
runId/sessionIdから同じ trace を引く。
process restart や deploy をまたいで approval を待つなら、memory 上の state だけでは足りない。AI SDK 7 の WorkflowAgent は step 間の state を durable storage に保存し、interrupt や遅延 approval から再開できる。どの workflow に durable resume が必要かは、この failure injection の段階で切り分ける。
repo に置く contract は小さくていい
実装ライブラリの設定とは別に、review で読める形を一枚置く。
# team-owned checklist。AI SDK の公式 schema ではない
agent_runtime:
approval_tools:
- publishPost
timeout_ms:
total: 60000
step: 10000
tool: 5000
trace_keys:
- runId
- sessionId
terminal_states:
- succeeded
- timed_out
- failed
これがあると、「新しい model のほうが賢いか」という話を始める前に、同じ失敗を同じ state と trace で再現できる。地味な順番だけれど、agent の運用で先に固定したいのは model ranking よりこちらだ。