スケジュールされたフォローアップ
エージェントが後回しの作業をスケジュールし、その結果を元のチャネルへ届ける仕組み — 永続的なタスク、アトミックな確保、そして復旧。
ターンの中では答えられないこともあります。数分かかるチェック、来週火曜日のリマインダー、6週間後に始める更新の会話などです。エージェントはその作業を登録し、準備ができたときに答えが届くようにできます。
その仕組み
ターンの途中で、エージェントはターン内に終わらない作業のために schedule_followup を呼びます。数分かかるチェック、来週火曜日のリマインダー、6週間後の更新の会話などです。
タスクはブローカー内のメッセージではなく、Postgresの行です。保留中のタスクはコミット済みのデータなので、再デプロイやクラッシュでも失われません — 永続性は、すでに使っているストアから無料で得られます。
ポーラーが数秒ごとに起き、期限が来たタスクを探します。コストは粒度です。タスクはミリ秒単位ではなく次のポーリングで発火しますが、分から週の単位で測られるフォローアップにとって、それは意味のある損失ではありません。
期限が来たタスクは FOR UPDATE SKIP LOCKED で確保されます。ポーラーが1つなら必要ありませんが、今それを正しく書いておくことで、後で2つ目のインスタンスが二重送信できなくなります。フォローアップが2回届くことは、送り手に対する顧客の信頼を損ないます。
結果が生成されます。プロセスに確保された後にそのプロセスが死んだタスクは、進行中のまま永遠に留まるのではなく、復旧パスによって保留中へ戻されます。
配信はリクエストが来たチャネルへ戻ります — Webウィジェットの吹き出し、TelegramやWhatsAppのメッセージです。WhatsAppで尋ねた人には、WhatsAppで答えます。
ツールは即座に返ります — 顧客は今すぐ返答を受け取り、スケジュールされた結果は後から届きます。
ツールはターンをブロックしません。行を書いて返るので、顧客は今すぐ返答を受け取り、スケジュールされた結果は後から届きます。
なぜキューがデータベースのテーブルなのか
Redisもブローカーもありません。タスクはPostgresの行であり、それは意図的なトレードオフです。
- 再起動で作業を失いません。 保留中のタスクはコミット済みの行です。プロセスは処理の途中で再デプロイでき、戻ってきたときにポーラーがそのタスクを拾います — 永続性は、独自の障害モードを持つ2つ目のシステムからではなく、すでに使っているデータストアから無料で得られます。
- 運用するものが1つ減ります。 単一プロセスのデプロイなら、ポーラーのループで十分です。
コストはレイテンシの粒度です。タスクはミリ秒単位ではなく次のポーリングで発火します。分から週の単位で測られるフォローアップにとって、それは意味のある損失ではありません。
確保はアトミック
期限が来たタスクは FOR UPDATE SKIP LOCKED で確保されます。ポーラーが1つなら必要ありませんが、今それを正しく書いておくことで、後で2つ目のインスタンスを走らせても二重送信しなくなります。フォローアップが2回届くのは見た目上のバグではありません。「更新の期限です」という同じメッセージを顧客が2回受け取り、送り手への信頼を失うということです。
SKIP LOCKED はまた、遅いタスクがその後ろでキューをブロックしないことも意味します。他のワーカーはロックされた行を飛び越え、次の行を取ります。
復旧
プロセスに確保された後にそのプロセスが死んだタスクは、放っておけば進行中のまま永遠に留まります。復旧パスは、行き詰まったタスクを保留中へ戻し、静かに取りこぼされるのではなく再試行されるようにします。
配信は起点をたどる
結果は、プラットフォームが選んだチャネルではなく、リクエストが来たチャネルへプッシュし戻されます。WhatsAppで尋ねた人には、WhatsAppで答えます。テナント自身のスケジュールされたアウトリーチ — 更新のリマインダー、締め切りの督促 — も、あなたが設定したリズムで同じように動きます。
これが実務上意味すること
- エージェントは「これについては後ほどお返事します」と言い、実際にそれを実行できます。
- リマインダーはデプロイ、再起動、クラッシュを生き延びます。
- フォローアップは一度だけ届き、相手があなたと話していた場所で相手に届きます。
誰かが覚えていることに依存しないフォローアップのビジネス上の論拠はあなたの時間を取り戻すにあります。スケジュールはツールとMCPで説明されている組み込みツールの1つです。