AIエージェントに外部APIやシェルを任せているなら、「落ちたら再試行」の一行を今日見直してほしい。
Workerは3台。停止を繰り返す間隔の設定は15〜40秒。それでも、タスクの投入は1回だけだ。
- 実行中のWorkerが落ちたら、別のWorkerが処理を引き継ぐ。
- 会話だけでなく、作業中のファイルも復元する。
- ただし、結果を保存できなかったツール呼び出しは、そのまま再実行しない。
それが、Temporalが2026年10月8日の技術記事で公開した、Piエージェントの分散実行実験だ。掲載ログには、復旧時にモデルへ返す次の一文がある。
The outcome of this tool call is unknown.
失敗した、ではない。何が起きたか分からない、である。
ここを同じ扱いにすると、復旧機能が二重実行の入口になる。今回はTemporalの実験を入口に、DBOSとRestateを含む3方式を「何を保存し、何をやり直すか」で比較する。

AI生成の概念イラスト。配送の完了と、その記録の到着が別であることを表したものだ。
情報確認日は2026年10月9日。新しい題材はTemporalの10月8日記事、DBOSの9月29日更新、Restateの9月30日発表から比較した。実験の数値はTemporalの公式記事・デモ設定からの引用であり、筆者の測定ではない。以下の比較は当日確認した一次資料の整理で、性能順位ではない。SDK・デモの実行、障害復旧の追試は行っていない。
数字で見る、公式デモの壊し方
まずデモのREADMEから設定と条件を整理する。表の数値はデモの構成・既定値で、稼働率や復旧時間の測定値ではない。
| 項目 | 数値・設定 | 読み違えてはいけない点 |
|---|---|---|
| デモのWorker数 | 3台 | Docker上の実験構成 |
| タスクの投入回数 | 1回 | 停止のたびにクライアントが再投入する方式ではない |
| 停止間隔の設定 | 15〜40秒 | 進行状況を待つ処理もあり、厳密な等間隔ではない |
| 実行中Workerを狙う割合の設定 | 70% | 観測された障害率ではない |
| 停止後に戻すまでの設定 | 5秒 | エージェントの復旧完了時間ではない |
| デモの待機上限 | 1,200秒=20分 | サービスのSLAではない |
| Workerの再起動方式 | replace |
既定では新しいコンテナへ置き換える |
| 停止機会を作る処理 | 3コマンドが各30秒sleep | モデルの推論時間ではない |
| ホスト側の公開ポート | UI:8233 / Temporal:7243 | 通常の開発サーバーの設定と混同しない |
15〜40秒という数字を、本番の耐障害性スコアに読み替えてはいけない。見るべきものは、停止後にどの処理が再び走ったかだ。
ここまでは「落ちても続く」デモの話。面白いのは、続けるために再実行を避ける部分だ。
なぜ「成功か失敗か」の2択では足りないのか
たとえば、エージェントが外部サービスに配送依頼を出す場面を考える。これは説明用の例だ。
① 実行を始める記録を保存
② 配送APIが依頼を受理
③ Workerが停止
④ 完了記録は保存されなかった
復旧したWorkerが見られるのは①までだ。②の成否は、ローカルの記録だけでは決まらない。
「結果がないから、もう一度送る」でよいのは、同じ依頼を何度送っても配送が増えない仕組みがある場合だ。その性質が冪等性である。
TemporalのActivity仕様も、ここを隠していない。
Temporal recommends that Activities be idempotent.
Activityが外部処理を終えても、サーバーへ完了を報告する前に落ちれば再試行され得る。Workflowの履歴があることと、外部の配送依頼が一度だけになることは別問題なのだ。
Pi実験が追加したのは、実行前のclaim
今回の実験では、ツールを呼ぶ前に「この呼び出しを開始する」というclaimを共有ストレージへ記録する。結果は別に保存する。この復旧手順を状態表にすると、判断は次の3つになる。
| 復旧時の証拠 | 分かること | 次の動作 |
|---|---|---|
| 保存済み結果がある | 返すべき結果が記録されている | 保存値を返す |
| claimだけある | 開始を試みたが、外部での成否は不明 | 不明をモデルへ返し、現状確認へ進む |
| claimも結果もない | この呼び出しの開始記録がない | claimを排他的に取得して実行へ進む |
要点だけを擬似コードにするとこうなる。未実行の説明用コードであり、原子的なclaim取得や永続化の実装は省略している。
def dispatch(call_id):
if result_exists(call_id):
return saved_result(call_id)
if not try_claim_once(call_id):
return OUTCOME_UNKNOWN
result = run_tool(call_id)
save_result(call_id, result)
return result
claimを書いた直後、ツールを呼ぶ前に落ちても「不明」になり得る。これは欠点を隠す言い換えではない。未実行かもしれない処理を、成功済みかもしれないまま再送しないための保守的な選択だ。
ただし、モデルが確認後に新しい呼び出しを作ることはある。配送APIに受付IDで照会する手段がなければ、「確認してから再試行」という計画自体が成立しにくい。
自分のエージェントなら、どのツールがここで止まると思う? あなたの予想もコメントで教えてほしい。
会話・実行・ファイルは、同じ場所にあるとは限らない
pi-temporalの構成では、Workflowが制御を持ち、会話はセッションファイルに残る。fleetでは作業ファイルも別ホストへ運ぶ。
したがって、実行履歴だけが生き残っても、次のWorkerが必要な会話やファイルを読めなければ仕事は続かない。「どの関数から再開するか」と「その関数が読む状態をどこから戻すか」を別々に設計する必要がある。
この実験が避けるのは同じ不明ツール呼び出しの自動反復であり、任意の外部副作用が一度だけになる保証ではない。
本番に入れる前に試すべき2つの使い方
1. Temporalの公式デモで、別Workerへの引き継ぎを見る
Node.js/npm、Docker、python3を用意し、使い捨ての検証環境で公式の導入手順とデモ手順を使うこと。デモはAnthropicのAPIキーを環境変数またはキー用ファイルで受け取り、モデル利用料が発生する。
以下は未実行の公式コマンドだ。ANTHROPIC_API_KEYまたはANTHROPIC_API_KEY_FILEを設定済みのシェルで実行する。
git clone https://github.com/temporalio/pi-temporal
cd pi-temporal
./install.sh
demo/run.sh
見る箇所は「最後に答えたか」だけではない。どのWorkerが止まり、どのActivityが再試行され、どのツールが不明扱いになったかを追う。ログはdemo/logs/<run>/に残る。
2. DBOSで、保存済みstepを飛ばす復旧を比べる
同じ「再開」でも、DBOSの公式Quickstartは、3ステップのworkflowをプロセス停止・再起動で回復させる入口だ。Python 3.10以上のmacOS/Linux向けコマンドは次の通り。未実行である。
python3 -m venv dbos-app-starter/.venv
cd dbos-app-starter
source .venv/bin/activate
pip install dbos 'fastapi[standard]'
dbos init --template dbos-app-starter
python3 main.py
http://localhost:8000/でworkflowを開始し、途中でアプリを止め、同じディレクトリからpython3 main.pyで再起動する。
Python版はSQLiteが既定だ。これは保存ファイルが残るプロセス障害を試す手順であり、そのディスクごと消す実験ではない。複数サーバーでの運用にはPostgresを使う。
この2つを比べると、「別ホストが仕事を引き継ぐ」と「同じ保存先から未完了の処理を戻す」の前提差が見える。
知らないと損する4つの罠
罠1:完了履歴が一度なら、外部処理も一度だと思う
Temporalの仕様は、Activityの完了を一度として観測しても、実体は複数回実行され得ると説明している。完了報告を失った試行があるからだ。
実行基盤に同じタスクを再投入しない仕組みと、その内部で呼ぶAPIの重複排除は、守る場所が違う。
対策:同じ業務操作には再試行をまたいで同じ冪等性キーを使い、呼び出し先が重複をどう扱うか確認すること。
罠2:Workerを増やせば、状態も勝手に引っ越すと思う
pi-temporalの設定仕様では、fleetのPI_SESSION_DIRに共有ストレージが必要で、全クライアント・Workerで同じ絶対パスに解決される必要がある。
自分のPCで復旧した経験だけでは、別マシンで同じファイルを読める証拠にならない。
対策:プロセス停止とホスト喪失を分け、次のWorkerから会話・claim・結果・作業ファイルを読めるか試すこと。
罠3:タイムアウトしたから、古い処理は止まったと思う
Temporalの実験報告は、待つのをやめても旧試行が動き続ける場合を扱っている。leaseを失ったWorkerの遅い会話追記は拒否できても、外部ツールの副作用まで取り消せるわけではない。
つまり、新Workerの「存在しない」という確認の後で、旧Workerの外部処理が完了する可能性も考える必要がある。
対策:遅れて戻る旧試行も障害試験に入れ、外部サービス側の重複排除や世代番号の検証まで設計すること。
罠4:保存済みstepがあるから、途中のコードも自由に変えられると思う
DBOSのworkflow仕様は、同じ入力とstep結果から、同じ順序・同じ引数でstepを呼ぶ決定性を要求する。workflow直下で現在時刻や乱数を使って分岐すると、再開時に別の道へ進み得る。
Restateにも、再試行で値が変わらない時刻・乱数のAPIがある。単に結果を保存するだけでなく、そこへ至る経路を再現する設計だ。
対策:非決定的な処理を記録対象の境界へ入れ、実行中workflowと互換性のあるコードで再開すること。
復旧基盤3方式を比較する
比較した新しい話題は、Temporalの10月8日の実験、DBOSの9月29日更新、Restateの9月30日の設計説明を含む発表である。最後のものは資金調達発表であり、新機能のリリース日という意味ではない。
選ぶ軸は「一番落ちにくい製品」ではなく、どこに状態と再試行の責任を置くかだ。
次の表は、DBOSの構成資料、Restateの構成資料と前掲のTemporal資料から整理した。Temporal欄は汎用製品全体の制約ではなく、今回のPi実験のfleet構成を指す。
| 比較軸 | Temporal+Pi実験 | DBOS | Restate |
|---|---|---|---|
| 主な永続化先 | 実行履歴+共有セッション・作業ファイル | system database。分散構成はPostgres | 複製ログ。状態はRocksDBへ構築 |
| 再開の基本単位 | 分割したモデル・ツール・確定処理 | workflow内のstep |
ctx.runなど、記録された操作 |
| 実行先への配布 | WorkerがTask Queueを取得 | アプリ側がDBのキューを取得 | ランタイムからhandlerへpush |
| 保存済み結果 | 記録を利用して処理を続ける | 保存値を返してstep本体を飛ばす | journalの結果を再生する |
| 未記録の外部処理 | claimだけなら不明を返す追加設計 | stepの再実行に備えた冪等性が必要 | 外部副作用の再実行に備えた冪等性が必要 |
| 状態を引き継ぐ前提 | Temporalに加え、必要な共有ファイルが生存 | DBが生存し、対応する実行プロセスへ復旧 | ログと状態を復旧し、handlerを再開 |
DBOSはライブラリとPostgresを中心に組む設計だ。分散環境の障害検出・復旧先調整にはConductorなどが必要になる。DBだけ置けば、あらゆる構成で自動的に別Workerへ引き継ぐわけではない。
Restateは複製ログを正本とし、quorum、つまり必要数の複製先による確認を永続化の境界に置く。RocksDBの状態はログから再構築でき、スナップショットも復旧を助ける。さらにVirtual Objectなら同じkeyへの書き手を1つに絞れるため、ユーザーや会話単位の状態所有を表現しやすい。
ただしRestateの副作用の説明も、結果が永続化される前の障害では実行が重複し得ると明記している。内部ログのcommitと、外部APIのcommitを混同してはいけない。
表から読み取れること:
- 既存の会話・作業ファイルを引き継ぎたいなら、Pi実験の状態分離とclaim方式が参考になる。
- アプリとPostgresを中心に運用したいなら、DBOSの保存・復旧モデルから検討できる。
- 会話IDなどを軸に状態の書き手を制御したいなら、RestateのVirtual Objectが比較候補になる。
- 照会も重複排除もできない外部処理には、どれを選んでも追加設計が必要だ。
以上は仕様からの選定上の見立てである。同条件の性能測定による順位ではない。
読み取りと書き込みで、再試行の設定を分ける
たとえばRestateでは、公式のretry policyから次のような設定を組める。既存のTypeScript handlerへ追加する未実行の設定断片だ。ctxはRestateのcontext、requestIdは照会対象のIDで、lookupStatusには自分の読み取り専用APIを実装する。
const retryPolicy = {
initialRetryInterval: { milliseconds: 500 },
retryIntervalFactor: 2,
maxRetryInterval: { seconds: 1 },
maxRetryAttempts: 5,
};
const status = await ctx.run(
"lookup-status",
() => lookupStatus(requestId),
retryPolicy,
);
これは照会の再試行を制御する例である。同じ設定を配送依頼などの書き込みへ貼るなら、回数を決める前に重複排除の契約が必要になる。
教訓:再試行回数より先に、完了の証拠を決める
✗ "結果がないから失敗した" → 外部では完了し、記録だけ失った可能性がある
✗ "履歴が残るから全部戻る" → 会話や作業ファイルが別の保存先にある場合がある
✗ "タイムアウトしたから止まった" → 古い試行が後から副作用を起こし得る
✗ "保存済みstepならコード変更も自由" → 再生する順序や分岐の互換性が必要だ
✗ "冪等性キーを付けたから安全" → 呼び出し先が同じキーを重複排除して初めて効く
エージェントの実行基盤を選ぶとき、モデル呼び出しの書きやすさだけでは決まらない。再開に必要な状態を残し、結果が不明な操作には照会手段を用意し、その答えが得られないときの停止・人への引き継ぎまで決める。そこが、デモと運用の間にある設計だ。
最初から全ツールを作り直す必要はない。まず、二度走ると困る処理を1つ選ぶ。その処理の外部完了とローカル記録の間にWorkerを落としたらどうなるか、説明できるかを確かめる。今日、自分のエージェントの「結果不明」を1つ洗い出してほしい。
参考資料
Temporal: The immortal life of Pi(2026年10月8日)
https://temporal.io/blog/the-immortal-life-of-pi-running-the-pi-coding-agent-on-temporal
temporalio/pi-temporal
https://github.com/temporalio/pi-temporal
pi-temporal: Chaos demo
https://github.com/temporalio/pi-temporal/blob/main/demo/README.md
Temporal: Activity Definition
https://docs.temporal.io/activity-definition
DBOS: What's New in DBOS - September 2026(2026年9月29日)
https://www.dbos.dev/blog/whats-new-in-dbos-september-2026
DBOS Architecture
https://docs.dbos.dev/architecture
DBOS: Workflows
https://docs.dbos.dev/python/tutorials/workflow-tutorial
DBOS: Get Started
https://docs.dbos.dev/quickstart
DBOS Database Connections
https://docs.dbos.dev/python/tutorials/database-connection
Restate: Series A発表と設計説明(2026年9月30日)
https://restate.dev/blog/announcing-series-a
Restate Architecture
https://docs.restate.dev/references/architecture
Restate: Services
https://docs.restate.dev/foundations/services
Restate: Durable Steps
https://docs.restate.dev/develop/ts/durable-steps
Restate: Why we built Restate(初期設計の副作用に関する説明)
https://restate.dev/blog/why-we-built-restate
エージェントの復旧設計に使えそうなら、いいねと保存を。外部APIやシェルを任せているチームにも、この比較を共有してほしい。
コメントで教えてほしい。
- 結果不明なら、自動照会・人の承認・冪等な再送のどれを選ぶ?
- あなたの構成では、実行履歴・Postgres・key単位の状態管理のどこを中心にしたい?
- 二度走ると一番困るツールは何だと思う?