Rider は、何かを実行する唯一のコンポーネントであり、モデルは一切実行しません。Rider は FlowSpec、具体的な入力、注入された capability のセットを受け取り、Journal を伴う Run を生成します。このページでは、Rider のスケジューリング方法、決定論性を保つ方法、Run を再開およびリプレイする方法を規定します。

1. Rider のシグネチャ

export interface Rider {
  run(spec: FlowSpec, inputs: JsonValue, deps: RunDeps): Promise<Run>;
  resume(runId: string, deps: RunDeps): Promise<Run>;
}

/**
 * エンジンに必要なものはすべて注入される (T2/T4)。エンジンは cloud SDK
 * を import せず、global を読まず、clock/RNG/network/fs/secrets
 * にはこれらを通してのみ触れる。実行場所を変えるには実装を差し替える。
 */
export interface RunDeps {
  store: Store;        // Run + Journal を永続化する(SQLite がデフォルト)
  journal: Journal;    // append-only の event sink
  tools: ToolHost;     // Registry から ToolContracts を解決 + 呼び出す
  clock: Clock;        // now(): string — replay 時に決定論的
  random: Random;      // next(): number — seeded、replay 時に決定論的
  secrets: Secrets;    // resolve(ref): string — `secrets` 対応 tools のみ
  env: EnvView;        // allow-listed environment values
  logger: Logger;      // structured、secret-masked
  signal?: AbortSignal;// cancellation
}
ファクトリ が Rider を構築し(createRider(config))、あらゆる config をクロージャに保持します。class でも singleton でもありません(このコードベースのスタイル)。

2. スケジューリング

Rider は 03 §6 で導出された DAG をたどります。
1

準備完了セット

ある手順は、それが needs するすべての手順が成功した(またはスキップされた)うえで、when 述語があれば truthy に評価されたときに ready になります。
2

ディスパッチ

ready な手順は並行実行され、config.maxConcurrency によって上限が設定されます(launcher が選択し、local ではデフォルト = CPU-bound)。
3

foreach

foreach を持つ手順は、配列に対して N 個の子呼び出しへ fan out し、それぞれが独立した context(c.item, c.index)を持ちます。それらの出力は配列として集約され、その手順の出力になります。子の並行数にも上限があります。
4

完了

ready な手順がなく、実行中の手順もない場合、Run は完了です。output 式が最終 context に対して評価され、検証されます。
決定論性の注記: 独立した手順は 並行 実行されますが、Journal は結果が commit される順序に従って effect の total order を記録し、foreach の集約は完了時刻ではなく index によって行われます。この記録された順序は 表示上のもの です。リプレイと再開は、journal position ではなく step ids と冪等性キーのみを厳密に key とするため、2つの Run が並行手順を異なる順序で commit しても、同一にリプレイされます。並行性は記録された結果を変えず、wall-clock だけを変えます(tenet T2)。

3. 手順のライフサイクル

各手順は小さな状態機械です。すべての遷移は Journal entry です。
export const JournalEntrySchema = z.discriminatedUnion("event", [
  z.object({ event: z.literal("run_started"), at: z.string(), flow_digest: z.string(), inputs_digest: z.string() }).strict(),
  z.object({ event: z.literal("step_started"), at: z.string(), step: z.string(), attempt: z.number().int() }).strict(),
  z.object({ event: z.literal("step_succeeded"), at: z.string(), step: z.string(), output_digest: z.string() }).strict(),
  z.object({ event: z.literal("step_failed"), at: z.string(), step: z.string(), error: DomainErrorSchema }).strict(),
  z.object({ event: z.literal("step_skipped"), at: z.string(), step: z.string(), reason: z.string() }).strict(),
  z.object({ event: z.literal("effect_recorded"), at: z.string(), step: z.string(), key: z.string(), result_digest: z.string() }).strict(),
  z.object({ event: z.literal("run_finished"), at: z.string(), status: z.enum(["succeeded", "failed", "canceled"]) }).strict(),
]);
Journal は append-only かつ content-addressed です(すべての出力と effect は digest によって保存されます)。これにより、再開とリプレイが可能になります。

4. リトライ、タイムアウト、冪等性

  • リトライポリシー は手順ごとに設定されます(retry: { max, backoff: "exp"|"fixed", base_ms, jitter })。Tool の kind から導かれる妥当なデフォルトがあります(network tools は retry し、pure transforms は retry しません)。
  • タイムアウトtimeout_ms)は注入された AbortSignal を通して手順を abort します。abort された手順は retryable failure です。
  • 冪等性: 各手順呼び出しは、安定した 冪等性キー = hash(run_id, step_id, inputs_digest) を持ちます。これは意図的に attempt number を含みません。そのため、同じ論理呼び出しのすべての retry は 同じ key を運びます。この安定性こそが目的です。外部 write を行う Tool(例: http.post)はこの key を受け取り、それを通過させることができます(例: Idempotency-Key header として)。これにより、retry された Run が二重発火しません。エンジンは、記録済み effect が再開時に再実行されないことを保証します(§6)。
冪等性キーが attempt number を含まないのは意図的な設計です。1 回目の attempt も 5 回目の attempt も 同じ key を運ぶからこそ、Tool 側は「これは前と同じ論理呼び出しだ」と判断でき、retry の二重発火を防げます。

5. 決定論性: 非決定性を許す場所

同じ入力と同じ記録済み effect が与えられたときに同じ出力を生成するなら、その Flow は決定論的です。Rider は、非決定性のすべての源を注入された capability に閉じ込めます。
発生源Capability再現可能性を保つ方法
現在時刻clockclock.now() は初回 read 時に記録され、replay は記録済み value を返す
乱数randomrun id から seed され、replay は同一に re-seed する
ネットワークnetrequest(secret-redacted、§5a)+ response が Journal に記録され、replay は記録済み response を返す
ブラウザーbrowserbrowser Tool 自体が決定論的(記録済み locators)であり、その reads は記録される
ファイルシステムfsreads は記録され、writes は冪等性によって key 付けされた effects になる
環境envallow-listed keys のみ、Run 開始時に snapshot される
シークレットsecrets各 Run で fresh に解決される(記録されない)。宣言した tools にのみ注入される
capability をまったく宣言しない手順(例: transform)は pure function です。エンジンはそれを自由に再実行でき、記録する必要もありません。

5a. シークレットを漏らさずに effect を記録する

secret は expression layer を決して通過しないため(taint rule、01 §7)、secret value が materialize する唯一の場所は、解決する tool の 内部 です。そしてそこは、まさに effect を記録する layer でもあります。そのため recorder は、どの bytes が secret-derived かを把握し、それらを redact します。secret-bearing な header、query parameter、body field は stable placeholder({secret:<slot>})として journaled され、value は記録されません。hermetic replay では、記録済み response は redacted request shape と idempotency key によって key 付けされるため、secret は不要であり、Journal にも存在しません。effect が記録されても、boundary invariant は保たれます。 大きな body や binary body は inline ではなく、blob store に digest によって保存されます。Journal は digest を保持し、エンジンは必要に応じて body を stream します。設定可能な size cap により Journal は小さく保たれ、上限を超える payload は artifact reference になります。

6. 再開

Run は中断されることがあり(crash、deploy、SIGTERM、明示的な cancel)、その Journal から再開できます。
1

Journal を読み込む

runId の Journal を読み込みます。
2

context を再構築する

step_succeeded entry と effect_recorded entry を replay して context を再構築します。完了済み手順は 記録済み出力 を返し、記録済み effect は 記録済み結果 を返します。外部のものは何も再発火しません。
3

frontier から続行する

ready set を再計算し、frontier から続行します。
完了済み work は Journal 内で content-addressed なので、再開は正確です。10 手順中 9 手順目で失敗した Flow は、1 手順目ではなく 9 手順目から再開し、手順 1-8 は再実行されません(tenet T7)。
Secrets は唯一の例外です。再開時には live に再解決され、Journal から読まれることはありません。

7. リプレイ

リプレイは再開の offline cousin であり、再現性の中核です(tenet T3)。2つの mode があり、決して混同されません。
browserflow replay <run-id>            # hermetic(デフォルト): いかなる種類の external call も行わない
browserflow replay <run-id> --verify   # hermetic + すべての step output が一致することを assert
browserflow replay <run-id> --rerun    # effects を live に再実行する(writes が再発火する)
デフォルトの hermetic mode では、注入された netclockrandomfs-read、browser capability は 完全に Journal によって裏付けられます。外部 call は行われずsecret も不要です。記録済み effect は記録済み結果を返します。これにより、3つの具体的な力が得られます。
  • 監査 — 何か月後でも、Run が何をしたかを決定論的に証明する。
  • 回帰テスト — Journal を fixture として pin する。Tool upgrade によって behavior が変わると、--verify は失敗する。
  • エアギャップ — FlowSpec + その Pack + 記録済み Journal を隔離環境へ移し、connectivity ゼロで結果を再現する。
opt-in の --rerun mode は異なります。これは Journal から reads を replay しますが、effects は live に再実行 するため、http.post は実際にもう一度発火します。この mode には関連する secrets が必要です。secrets は実行時に再解決され、Journal から読まれることはありません。secrets は記録されないためです(§5a)。既知の開始点から live system に対して Flow を再駆動するには --rerun を使い、何にも触れない hermetic reproduction には通常の replay / --verify を使います。

8. エラー

エラーは DomainError lineage に従います(このコードベースのスタイルで class syntax が許される唯一の場所: Error subclasses)。それぞれが stable な code、問題のある step、(masked)inputs、Journal frame を持つため、failures は actionable であり、secret を漏らすこともありません。
export const DomainErrorSchema = z
  .object({
    code: z.string().min(1), // e.g. "tool.http.timeout", "tool.browser.locator_missing"
    message: z.string().min(1),
    step: z.string().optional(),
    retryable: z.boolean(),
    cause_digest: z.string().optional(),
  })
  .strict();

9. エンジンが意図的に行わないこと

  • エンジンは control flow を決めるためにモデルを呼びません。(そのような code path は存在しません。)
  • エンジンは ambient globals に手を伸ばしません。注入された EnvView の外で Date.now()Math.random()process.env reads を行わず、bare fetch も使いません。
  • エンジンは secrets を永続化しません。
  • エンジンは cloud SDK を import しません。storage/queue/transport は RunDeps です。
これらの omission は feature です。同じ FlowSpec をあらゆる場所で実行し、再現できるのは、このためです。

Launcher

local、server、air-gapped environment をまたいで、同じ FlowSpec を実行・再現します。

Tool と Pack

ToolContracts がどのように解決され、呼び出され、portable Packs に bundle されるか。