defmodule PromptRunner.SimulatedLLM do @moduledoc """ Built-in deterministic provider used for retry, repair, and resume demos. """ @behaviour PromptRunner.LLM alias PromptRunner.FailureEnvelope @impl true def normalize_provider(_value), do: :simulated @impl true def normalize_sdk(_value), do: :simulated @impl true def start_stream(llm, _prompt) when is_map(llm) do attempt = llm[:attempt] || 1 step = simulation_attempt(llm, attempt) meta = simulation_meta(llm, attempt) apply_writes(step, llm) {:ok, simulation_stream(step, meta), fn -> :ok end, meta} end @impl true def resume_stream(llm, meta, _prompt) when is_map(llm) and is_map(meta) do step = simulation_resume(llm, meta) apply_writes(step, llm) {:ok, simulation_stream(step, meta), fn -> :ok end, meta} end defp simulation_attempt(llm, attempt) do llm |> simulation_config() |> Map.get("attempts", []) |> Enum.at(max(attempt - 1, 0), %{}) |> normalize_map() end defp simulation_resume(llm, meta) do llm |> simulation_config() |> Map.get("resume", %{}) |> normalize_resume(meta) end defp simulation_config(llm) do llm[:simulation] || llm["simulation"] || %{} end defp simulation_meta(llm, attempt) do provider_session_id = "simulated-#{llm[:prompt_id] || "prompt"}-#{attempt}" %{ sdk: :simulated, provider: :simulated, model: llm[:model] || "simulated-demo", cwd: llm[:cwd], provider_session_id: provider_session_id, simulation: simulation_config(llm) } end defp simulation_stream(step, meta) do [ %{type: :run_started, data: run_started_data(meta, step)} | step_messages(step) ++ [finish_event(step)] ] end defp run_started_data(meta, step) do %{ model: meta.model, provider_session_id: meta.provider_session_id, metadata: %{"simulated" => true}, confirmation_source: "simulated" } |> maybe_put(:simulation_label, step["label"]) end defp step_messages(step) do step |> Map.get("messages", []) |> Enum.map(fn message -> %{type: :message_streamed, data: %{delta: to_string(message)}} end) end defp finish_event(%{"error" => error}) when is_map(error) do provider_error = simulated_provider_error(error) %{ type: :error_occurred, data: %{ error_message: provider_error.message, provider_error: provider_error } } end defp finish_event(_step) do %{type: :run_completed, data: %{stop_reason: "end_turn"}} end defp apply_writes(step, llm) do Enum.each(step["writes"] || [], &apply_write(&1, llm)) end defp apply_write(entry, llm) when is_map(entry) do path = resolve_write_path(entry, llm) File.mkdir_p!(Path.dirname(path)) content = entry["text"] || "" if entry["append"] in [true, "true", "TRUE", 1, "1"] do File.write!(path, content, [:append]) else File.write!(path, content) end end defp apply_write(_entry, _llm), do: :ok defp resolve_write_path(%{"repo" => repo, "path" => rel_path}, llm) when is_binary(repo) and is_binary(rel_path) do case get_in(llm, [:repo_paths, repo]) || get_in(llm, ["repo_paths", repo]) do root when is_binary(root) -> Path.join(root, rel_path) _ -> resolve_relative_path(rel_path, llm[:cwd]) end end defp resolve_write_path(%{"path" => rel_path}, llm) when is_binary(rel_path) do resolve_relative_path(rel_path, llm[:cwd]) end defp resolve_write_path(_entry, llm), do: resolve_relative_path("simulated.out", llm[:cwd]) defp resolve_relative_path(path, cwd) do if Path.type(path) == :absolute, do: path, else: Path.expand(path, cwd || File.cwd!()) end defp normalize_resume(list, meta) when is_list(list) do Enum.at(list, resume_index(meta), %{}) |> normalize_map() end defp normalize_resume(map, _meta) when is_map(map), do: normalize_map(map) defp normalize_resume(_other, _meta), do: %{} defp resume_index(meta) do meta[:resume_index] || meta["resume_index"] || 0 end defp normalize_map(map) when is_map(map), do: Map.new(map, fn {k, v} -> {to_string(k), normalize_value(v)} end) defp normalize_map(_other), do: %{} defp normalize_value(value) when is_map(value), do: normalize_map(value) defp normalize_value(value) when is_list(value), do: Enum.map(value, &normalize_value/1) defp normalize_value(value), do: value defp normalize_error_kind(kind) when kind in [ :protocol_error, :transport_error, :transport_exit, :transport_timeout, :provider_rate_limit, :user_cancelled, :approval_denied, :guardrail_blocked ], do: kind defp normalize_error_kind(kind) when is_binary(kind), do: kind |> String.downcase() |> normalized_binary_error_kind() defp normalize_error_kind(_kind), do: :runtime_error defp normalized_binary_error_kind("protocol_error"), do: :protocol_error defp normalized_binary_error_kind("transport_error"), do: :transport_error defp normalized_binary_error_kind("transport_exit"), do: :transport_exit defp normalized_binary_error_kind("transport_timeout"), do: :transport_timeout defp normalized_binary_error_kind("provider_rate_limit"), do: :provider_rate_limit defp normalized_binary_error_kind("rate_limit"), do: :provider_rate_limit defp normalized_binary_error_kind("user_cancelled"), do: :user_cancelled defp normalized_binary_error_kind("approval_denied"), do: :approval_denied defp normalized_binary_error_kind("guardrail_blocked"), do: :guardrail_blocked defp normalized_binary_error_kind(_kind), do: :runtime_error defp simulated_provider_error(error) do recovery = error |> Map.get("recovery", %{}) |> case do recovery when is_map(recovery) and map_size(recovery) > 0 -> normalize_map(recovery) _ -> %{ "class" => simulated_class(error["kind"]), "retryable?" => simulated_retryable?(error["kind"]), "repairable?" => true, "resumeable?" => simulated_resumeable?(error["kind"]), "remote_claim?" => true, "local_deterministic?" => false, "severity" => "error" } end failure = FailureEnvelope.from_reason(%{ provider_error: %{ provider: :simulated, kind: normalize_error_kind(error["kind"]), message: error["message"] || "simulated failure", recovery: recovery } }) %{ provider: :simulated, kind: normalize_error_kind(error["kind"]), message: error["message"] || "simulated failure", stderr: error["stderr"], truncated?: false, retryable?: failure.retryable?, recovery: recovery } end defp simulated_class(kind) when kind in ["provider_capacity", :provider_capacity], do: "provider_capacity" defp simulated_class(kind) when kind in ["provider_rate_limit", :provider_rate_limit, "rate_limit", :rate_limit], do: "provider_rate_limit" defp simulated_class(kind) when kind in ["provider_auth_claim", :provider_auth_claim, "auth_error", :auth_error], do: "provider_auth_claim" defp simulated_class(kind) when kind in [ "provider_config_claim", :provider_config_claim, "config_invalid", :config_invalid ], do: "provider_config_claim" defp simulated_class(kind) when kind in [ "provider_runtime_claim", :provider_runtime_claim, "runtime_error", :runtime_error ], do: "provider_runtime_claim" defp simulated_class(kind) when kind in ["protocol_error", :protocol_error], do: "protocol_error" defp simulated_class(kind) when kind in ["transport_error", :transport_error], do: "transport_disconnect" defp simulated_class(kind) when kind in ["transport_exit", :transport_exit], do: "transport_disconnect" defp simulated_class(kind) when kind in ["transport_timeout", :transport_timeout], do: "transport_timeout" defp simulated_class(kind) when kind in ["user_cancelled", :user_cancelled], do: "user_cancelled" defp simulated_class(kind) when kind in ["approval_denied", :approval_denied], do: "approval_denied" defp simulated_class(kind) when kind in ["guardrail_blocked", :guardrail_blocked], do: "guardrail_blocked" defp simulated_class(_kind), do: "provider_runtime_claim" defp simulated_retryable?(kind) do simulated_class(kind) not in ["user_cancelled", "approval_denied", "guardrail_blocked"] end defp simulated_resumeable?(kind) do simulated_class(kind) in ["protocol_error", "transport_disconnect", "transport_timeout"] end defp maybe_put(map, _key, nil), do: map defp maybe_put(map, key, value), do: Map.put(map, key, value) end