defmodule Pi.Self do @moduledoc """ Self-introspection facade for pi-elixir eval. `Pi.Self` is the compact dashboard/API boundary for asking the running bridge about itself: bridge inventory, eval state, sessions, plugins, QuackDB mirror, and recall context. Eval preloads `alias Pi.Self, as: Self` for concise use. """ use QuackDB.Ecto alias Pi.Bridge.Info alias Pi.Plugin.Manager alias Pi.Quack alias Pi.Skill.Loader, as: SkillLoader @default_context_limit 5 @doc "Returns a compact all-in-one bridge status map." def status(opts \\ []) do %{ bridge: info_summary(), eval: eval(opts), quack: quack(), sessions: sessions(), plugins: plugins(), skills: skills(), apis: apis() } end @doc "Returns the full bridge inventory snapshot." def info, do: Info.snapshot() @doc "Returns compact bridge environment metadata." def env do %{ cwd: File.cwd!(), mix_project: Mix.Project.config()[:app], mix_env: Mix.env(), elixir: System.version(), otp: System.otp_release(), node: Node.self(), features: %{ llm: Pi.Features.llm?(), sessions: Pi.Features.sessions?(), plugins: Pi.Features.plugins?(), mirror: Pi.Features.env_enabled?("PI_ELIXIR_MIRROR"), skills: Pi.Features.skills?() } } end @doc "Returns loaded runtime/extension API inventory." def apis do snapshot = Info.snapshot() %{runtime: snapshot.apis.runtime, extensions: snapshot.apis.extensions} end @doc "Returns eval alias/import prelude code." def aliases, do: Info.aliases_code() @doc "Returns current eval binding metadata." def bindings, do: Pi.Eval.bindings() @doc "Returns eval status metadata." def eval(_opts \\ []) do %{ bindings: bindings(), binding_count: length(bindings()) } end @doc "Returns active BEAM session snapshots." def sessions do %{ active: Pi.Session.snapshots(), count: length(Pi.Session.snapshots()) } catch :exit, reason -> %{error: Exception.format_exit(reason)} end @doc "Returns compact QuackDB mirror status." def quack do Quack.status() rescue exception in [RuntimeError, QuackDB.Error, DBConnection.ConnectionError] -> %{error: Exception.message(exception)} end @doc "Alias for `quack/0`, emphasizing storage status." def storage, do: quack() @doc "Returns loaded plugin metadata and commands." def plugins do if Pi.Features.plugins?() do %{ plugins: Manager.plugins(), commands: Manager.commands() } else %{disabled: true} end rescue exception in [RuntimeError, ArgumentError] -> %{error: Exception.message(exception)} end @doc "Returns discovered executable skills." def skills do if Pi.Features.skills?(), do: SkillLoader.serializable(), else: [] rescue exception in [RuntimeError, ArgumentError, File.Error, Code.LoadError, CompileError] -> %{error: Exception.message(exception)} end @doc "Returns a compact recall block from mirrored session history." def context(query, opts \\ []) when is_binary(query) do query |> recall(opts) |> format_context_block() end @doc "Returns structured recall rows from mirrored session history." def recall(query, opts \\ []) when is_binary(query) do import Ecto.Query require Quack limit = Keyword.get(opts, :limit, @default_context_limit) Quack.rebuild_fts!() from(e in Pi.Quack.Event, where: Quack.matches(e.id, ^query), order_by: [desc: Quack.score(e.id, ^query)], limit: ^limit, select: %{ score: Quack.score(e.id, ^query), id: e.id, event_type: e.event_type, cwd: e.cwd, session_file: e.session_file, tool: e.tool_name, content: Quack.json_text(e.payload_json, "$.content"), payload: e.payload_json } ) |> Quack.all() rescue exception in [RuntimeError, ArgumentError, QuackDB.Error, DBConnection.ConnectionError] -> [%{"error" => Exception.message(exception)}] end defp info_summary do snapshot = Info.snapshot() %{ project: snapshot.project, version: snapshot.version, transport: snapshot.transport, plugins: Enum.map(snapshot.plugins, & &1.name), commands: Enum.map(snapshot.commands, & &1.name) } end defp format_context_block([%{"error" => error}]), do: "" defp format_context_block([]), do: "" defp format_context_block(rows) do body = rows |> Enum.with_index(1) |> Enum.map_join("\n\n", fn {row, index} -> format_context_row(index, row) end) "\n" <> body <> "\n" end defp format_context_row(index, row) do content = clean_text(row["content"] || row["payload"] || "") source = Path.basename(to_string(row["session_file"] || "session")) tool = row["tool"] || row["event_type"] || "event" [ Integer.to_string(index), ". ", source, " ", to_string(tool), " score=", format_score(row["score"]), "\n", truncate(content, 1_200) ] |> IO.iodata_to_binary() end defp clean_text(text) do text |> to_string() |> String.replace(~r/\s+/u, " ") |> String.trim() end defp truncate(text, max) when byte_size(text) <= max, do: text defp truncate(text, max), do: String.slice(text, 0, max) <> "..." defp format_score(score) when is_float(score), do: :erlang.float_to_binary(score, decimals: 3) defp format_score(score), do: to_string(score || "?") end