Powered by AppSignal & Oban Pro

Detached runs, streams, and replay

guides/livebooks/02_detached_runs.livemd

Detached runs, streams, and replay

What you will learn

This notebook demonstrates the supervised run lifecycle. A detached run is owned by Jido.Harness rather than this Livebook cell, so later cells can inspect or reattach to it by run_id.

By the end you will have:

  • started and inspected one detached run;
  • consumed its pull-based cursor stream;
  • replayed bounded event pages;
  • awaited its normalized terminal result;
  • seen how cancellation and pruning fit the lifecycle.

Read the detached-runs guide and retention guide for the corresponding API contracts.

Provider run cells may consume API or subscription usage.

Install the local package

Open this notebook from the source checkout with Elixir 1.19 or a compatible later version. The relative dependency path uses its saved location. Configure the Livebook runtime's PATH before starting provider cells; see Runtime environment.

Mix.install([
  {:jido_harness, path: Path.expand("../..", __DIR__)}
])

defmodule JidoHarnessLivebook do
  def completed!({:ok, %{status: :completed} = result}), do: result

  def completed!({:ok, result}) do
    raise result.error || "provider run finished with status #{inspect(result.status)}"
  end

  def completed!({:error, %_{} = error}), do: raise(error)
  def completed!({:error, error}), do: raise("provider run failed: #{inspect(error)}")
end

Start a detached run

provider = :codex
cwd = Path.expand("../..", __DIR__)

{:ok, provider_status} = Jido.Harness.status(provider)

unless Jido.Harness.ProviderStatus.ready?(provider_status) do
  raise """
  #{provider} is not ready in this Livebook runtime.
  executable: #{inspect(provider_status.executable)}
  error: #{inspect(provider_status.error)}
  PATH: #{System.get_env("PATH")}
  """
end

{:ok, run_id} =
  Jido.Harness.Run.start(provider, %{
    prompt: "Reply with exactly: detached-run-ready",
    cwd: cwd,
    runtime_timeout_ms: 300_000,
    metadata: %{source: "detached-runs-livebook"}
  })

run_id

The snapshot contains lifecycle state and an output cursor. The cursor is the last appended sequence; it does not count records still retained after rotation.

Jido.Harness.Run.info(run_id)

Attach to the event stream

The stream polls cursor-addressed retention rather than receiving an unbounded producer mailbox. Evaluating the next cell waits until the run reaches a terminal state.

{:ok, event_stream} = Jido.Harness.Run.stream(run_id, poll_interval_ms: 25)
events = Enum.to_list(event_stream)

Enum.map(events, fn event ->
  %{
    sequence: event.sequence,
    type: event.type,
    turn_id: event.turn_id,
    text: event.payload["text"]
  }
end)

Await and replay

Awaiting a finished run returns immediately. Replay can start at any cursor and provides retained events when a result's bounded text tail is truncated. Journals are bounded and are not recovered after an application restart.

result =
  run_id
  |> Jido.Harness.Run.await(300_000)
  |> JidoHarnessLivebook.completed!()

%{
  status: result.status,
  text: result.text,
  text_truncated?: result.text_truncated?,
  usage: result.usage
}
{:ok, first_page} = Jido.Harness.Run.replay(run_id, cursor: 0, limit: 5)
next_cursor = case List.last(first_page) do
  nil -> 0
  event -> event.sequence
end

{:ok, second_page} = Jido.Harness.Run.replay(run_id, cursor: next_cursor, limit: 5)

%{
  first_page: Enum.map(first_page, &{&1.sequence, &1.type}),
  second_page: Enum.map(second_page, &{&1.sequence, &1.type}),
  next_cursor: next_cursor
}

Cancellation and pruning

The following cell is intentionally disabled. Set exercise_cancellation? to true to start a second live request and request cancellation. Cancellation targets the managed process group. A request that already completed can keep its completed result.

exercise_cancellation? = false

if exercise_cancellation? do
  {:ok, cancellable_id} =
    Jido.Harness.Run.start(provider, %{
      prompt: "Wait briefly, then reply with exactly: cancellation-missed",
      cwd: cwd
    })

  :ok = Jido.Harness.Run.cancel(cancellable_id)
  cancelled = Jido.Harness.Run.await(cancellable_id, 30_000)
  :ok = Jido.Harness.Run.prune(cancellable_id)
  cancelled
else
  :skipped
end

Terminal runs remain available for the configured retention period. Pruning is explicit when the caller no longer needs the journal.

Jido.Harness.Run.prune(run_id)