実行
実行とは、1 行のレコード、カーソル、試行のジャーナル、期限の集まり、中断の集まりです。このページは、実行に対して何ができ、実行が何を返してくるかを説明します。
実行の開始
実行は、定義の id と入力から始まります。定義にスキーマがあればデーモンは入力を それに照らして検証し、保存されている最も高いバージョンを固定し、実行とその トレースを作り、保存されたとおりの実行を返します。そこから先、実行は自分で 進みます。デーモンはそのステップをワーカーに提示し、スリープと待機をたどり、 起きたことをジャーナルに書きます。
開始、応答、追跡
// Start a run from anywhere that holds an execute key, answer the wait from a
// webhook, then follow the journal to the end and read the run.
const run = await mcpd.runs.start({
definitionId: 'support_ticket',
input: { ticket: 'Q-42' },
})
await mcpd.events.publish({
event: 'approved',
key: 'Q-42',
payload: { by: 'ops' },
eventId: 'hook-1', // a repeat of this id is a duplicate, not a second event
})
for await (const event of mcpd.runs.watch(run.runId)) {
console.log(event.seq, event.kind)
}
const detail = await mcpd.runs.get(run.runId)
console.log(detail.run.status, detail.run.output) # Start a run from anywhere that holds an execute key, answer the wait from a
# webhook, then follow the journal to the end and read the run.
run = mcpd.runs.start(support, input={"ticket": "Q-42"})
# A repeat of this event id is a duplicate, not a second event.
mcpd.events.publish("approved", "Q-42", payload={"by": "ops"}, event_id="hook-1")
for event in mcpd.runs.watch(run.run_id):
print(event.seq, event.kind)
detail = mcpd.runs.get(run.run_id)
print(detail.run.status, detail.run.output) // Start a run from anywhere that holds an execute key, answer the wait from
// a webhook, then follow the journal to the end and read the run.
let start = StartRun::new("support_ticket").input(json!({ "ticket": "Q-42" }));
let run = mcpd.start_run(start).await?;
// A repeat of this event id is a duplicate, not a second event.
let payload = json!({ "by": "ops" });
let approval = PublishEvent::new("approved", "Q-42").payload(payload).event_id("hook-1");
mcpd.publish_event(approval).await?;
mcpd.watch_run(&run.run_id, -1, |e| println!("{} {:?}", e.seq, e.kind)).await?;
let detail = mcpd.get_run(&run.run_id).await?;
println!("{:?} {:?}", detail.run.status, detail.run.output); // Start a run from anywhere that holds an execute key, answer the wait from
// a webhook, then follow the journal to the end and read the run.
run, err := m.StartRun(ctx, mcpd.StartRun{
Definition: "support_ticket",
Input: map[string]any{"ticket": "Q-42"},
})
if err != nil {
panic(err)
}
// A repeat of this event id is a duplicate, not a second event.
_, err = m.PublishEvent(ctx, mcpd.PublishEvent{
Event: "approved",
Key: "Q-42",
Payload: map[string]any{"by": "ops"},
EventID: "hook-1",
})
if err != nil {
panic(err)
}
_ = m.WatchRun(ctx, run.RunID, -1, func(e mcpd.RunEvent) error {
fmt.Println(e.Seq, e.Kind)
return nil
})
detail, _ := m.GetRun(ctx, run.RunID)
fmt.Println(detail.Run.Status, string(detail.Run.Output)) // Start a run from anywhere that holds an execute key, answer the wait from
// a webhook, then follow the journal to the end and read the run.
val run = mcpd.startRun("support_ticket", input = mapOf("ticket" to "Q-42"))
// A repeat of this event id is a duplicate, not a second event.
mcpd.publishEvent("approved", "Q-42", payload = mapOf("by" to "ops"), eventId = "hook-1")
mcpd.watchRun(run.runId) { event -> println("${event.seq} ${event.kind}") }
val detail = mcpd.getRun(run.runId)
println("${detail.run.status} ${detail.run.output}") # Start a run from anywhere that holds an execute key, answer the wait from a
# webhook, then follow the journal to the end and read the run.
{:ok, run} = Mcpd.start_run(mcpd, support, input: %{"ticket" => "Q-42"})
# A repeat of this event id is a duplicate, not a second event.
{:ok, _} =
Mcpd.publish_event(mcpd, "approved", "Q-42",
payload: %{"by" => "ops"},
event_id: "hook-1"
)
mcpd |> Mcpd.watch_run(run.run_id) |> Enum.each(&IO.inspect({&1.seq, &1.kind}))
{:ok, detail} = Mcpd.get_run(mcpd, run.run_id)
IO.inspect({detail.run.status, detail.run.output})
リトライされる可能性のある呼び出し元からは idempotency_key を
送ってください。これは実行が保持されている間プロジェクト内で一意で、同じ
呼び出しのリトライは 2 つ目の実行を始めるのではなく同じ実行を返します。
webhook が二度発火しても、ジョブランナーが再起動しても、それで余計な実行が
生まれることはありません。
The cursor
The daemon holds the run between steps: where it is, what each step answered, what it is waiting for and until when. A worker holds one step at a time, and only while it runs. During a wait no process is alive at all.
ジャーナル
実行へのすべての変更は、ゼロから始まり欠番のない連番が付いたイベントです。 実行が始まった、カーソルが動いた、ステップがリースされた、開始した、終了した、 失敗した、実行が待機して再開した、実行が終わった、といったものです。それぞれが 種類に応じた形のペイロードを持ちます。ジャーナルは実行の履歴であり、 インターフェースが描いているものです。
after からページ単位で読むか、イベントのエンドポイントを
Accept: text/event-stream で開いたままにして、コミットされた順に
各イベントを受け取ってください。止まってから再開したクライアントは、最後に見た
連番を渡せば何も取りこぼしません。どの SDK の watch もこれを
代わりに行い、実行が終わると終了します。wait はジャーナルを最後まで
たどって出力を返し、成功しなかった場合は実行を含む型付きの失敗を返します。
イベントと待機
wait_event ステップは、その名前と展開されたキーを持つイベントが
届くまで実行を止めます。イベントは 1 つの入口から入ります。
待機に応える
curl -X POST http://localhost:4317/v1/api/events \
-H 'content-type: application/json' \
-d '{"project":"support","event":"approved","key":"Q-42","payload":{"by":"ops"},"event_id":"hook-1"}'
応答は 3 つの結果のいずれかです。delivered は、待機していた実行が
同じトランザクションでイベントを取り込んだという意味で、受領を読む時点で実行は
すでに進んでいます。parked は、待機していた実行がなく、あとで開く
待機のためにイベントが保持されている、という意味です。保持されたイベントを
受け取れるのは、保留イベントを受け付けると宣言した待機だけです。
duplicate は、その event_id を以前に見たので何も
書かれなかった、という意味です。二度発火しうるものからは、自分の id を送って
ください。
期限に達した待機は、その on_timeout の指定どおりに動きます。
失敗して on_error に判断を渡すか、出力なしで続行するかの
どちらかです。ある実行に対して、その実行自身の開いている待機を通じて直接
送る場合は POST /v1/api/runs/{run}/signal を使います。
実行そのものが宛先なので、キーなしで同じ本文を受け取ります。
キャンセル
POST /v1/api/runs/{run}/cancel は、実行とその下のすべての子
実行を終わらせます。実行中のステップが通信の途中で中断されることはありません。
ワーカーは次のハートビートで実行が終わったことを知り、そのコンテキストが本体に
合図を送り、本体がそのあとに送るものは run_terminal として拒否
されます。すでに終わった実行をキャンセルしても何も変わらず、そのままの実行が
返ります。
タイマーと上限
スリープ、待機の期限、リトライのバックオフ、リースの失効は、すべてデーモンが
持つタイマーです。これらはデーモンの時計で毎秒発火するので、100 ミリ秒の
retry_after_ms は「ちょうどそれだけ」ではなく「少なくともそれだけ」
として守られます。デーモンを再起動してもタイマーは失われません。それらは行で
あり、実行はその行から再開します。どの期間も 3650 日より長くはできず、
ワーカーから届いたそれより長い retry_after_ms は 400 で拒否
されます。2262 年より先の sleep_until は、期限が来ないものと
して保持されます。
定義の max_steps、run_timeout_ms、
max_cost_usd は、上限に達したところで、その上限をエラーとして
名指しながら実行を終わらせます。コストは、各ステップが報告した使用量を値付け
した合計で、スパンに値付けするのと同じ価格表を使います。したがってトークンを
報告するエージェントのステップは、完了した時点で実行の予算に計上されます。
トレースの中の実行
すべての実行はトレースでもあります。デーモンは実行のルートスパンと、試行ごとに
1 つのスパンを、ステップの入力と出力とともに開きます。ステップコンテキストを
通じて自分のスパンを記録する本体は、それらをその下に置きます。したがって
エージェントのステップの中のモデル呼び出しは、そのステップの下、その実行の下に
位置し、任意のトレースに対して働く検索、コスト計算、スコアラーが、実行の
トレースに対してもそのまま働きます。それらのスパンは SDK のスパンと同じ
取り込み経路を通って書かれるので、同じようにサンプリングされ、秘匿化され、
値付けされ、転送されます。トークンは、それを使ったモデルのスパンに
載ります。ステップのスパンは値付け済みの合計を属性に持つので、トレース全体の
合計は各トークンを一度だけ数えます。実行は自分の trace_id を持ち、
インターフェースが両者をつなぎます。
デーモンはそれらのスパンを、元になった行がコミットされたあとに、自分の時計で アウトボックス経由で書きます。ステップの完了直後に読んだトレースには、その ステップのスパンがまだ載っていないことがあります。実行自身の詳細には常に 載っています。
実行の読み出し
GET /v1/api/runs は、プロジェクトの実行を更新の新しい順に並べ、
状態や定義で絞り込みます。GET /v1/api/runs/{run} は、その実行
と、それについて永続化されているすべてです。リースの保有者、時刻、エラー、
使用量を伴う各試行、まだ発火していないタイマー、まだ開いている待機です。
インターフェースの
ワークフロー画面
は、このエンドポイントを描いたもので、その横にジャーナルが実時間で並びます。
どちらも実行が受け取ったものを返すので、どちらも監査ログに
list_runs と get_run として記録されます。
ジャーナルの読み取りは list_run_events として、ストリームも
含めてリクエストごとに 1 回、原因の追跡は get_step_cause と
して、キャンセルは cancel_run として、同じく記録されます。
拒否された読み取りは、その実行を所有するプロジェクトに対して記録されます。
ワーカーの呼び出しは記録されません。
ワーカーを参照してください。
ストレージ
実行は、トレースのストアの隣にある自分の SQLite ファイルに入ります。
--runs-data が別を指さない限り
<data>.runs.<ext> です。--runs-data off
にすると実行の面全体が止まり、その場合はすべての実行エンドポイントが 503 を
返します。トレースのストアと一緒にバックアップしてください。
mcpd doctor はこれを独立した項目として評価します。
実行ストアは、ペイロードを送られたとおりに保持します。実行の入力、状態、
出力、相関、各ステップの入力、出力、エラー、すべてのイベントのペイロード、
そしてトレースのストアへの書き込みを待っているスパンのペイロードは、秘匿化
されずに保存され、実行の相関にある user_id はハッシュ化されずに
保存されます。これは意図的です。それらはワークフローの作業状態であり、あとの
ステップがそれを読み返すので、秘匿化処理が書き換えた値は実行の動きを変えて
しまいます。デーモンがトレースのストアに書くスパンは、SDK が送るスパンと同じ
秘匿化と識別子のハッシュ化を通るので、トレースのストアにあるのは秘匿化された
写しです。実行のファイルは、ステップに渡したすべてを持つものとして扱い、
それに見合うように保護し、バックアップしてください。
保持期間は実行ストアにも及びます。トレースのストアを削る 1 時間ごとの処理は、 プロジェクトごとの同じ期間で、ルートが保持期間の境界より前に終わったすべての 実行も、その子の実行と、それらについて保存されたすべてとともに削除します。 試行、イベント、期限、待機、キューにあるスパン、そしてそれらの実行が取り込んだ イベントです。終わっていない実行は、どれだけ古くても削除されません。定義は 残ります。
消去も実行ストアに及びます。DELETE /v1/api/subjects/{user_id}
は、プロジェクト内の終わった実行のうち、相関がその user_id を、
実行を開始したときの形のまま持つものをすべて、子の実行の木全体と、それらに
ついて保存されたすべてとともに削除し、その件数を runs_erased
として返します。その個人の実行のうち終わっていないものは、まるごと残され、
runs_in_flight に数えられ、応答の note で名指し
されます。それをキャンセルするか終わるのを待ってから、消去をもう一度送って
ください。まだ待機に達していない実行のために保持されたイベントは、個人では
なく相関キーに結びついているので、消去は届きません。それは期限が切れたとき、
つまり 24 時間後に削除されます。応答の残りについては
マスキングと保持を参照
してください。
次に
- 定義に書けること。 ワークフロー。
- ステップを処理するプロセス。 ワーカー。
- 仕様から生成された実行エンドポイントの全体。 HTTP API、Runs。