Packages
A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.
Retired package: Deprecated - superseded — operator console moved to the Syntropy app
Current section
Files
Jump to
Current section
Files
lib/syntropy_web/live/agent_live.ex
defmodule SyntropyWeb.AgentLive do
use SyntropyWeb, :live_view
import SyntropyWeb.RuntimeViewHelpers
alias Syntropy.{HistoryStore, LatticeSupervisor, RecommendationStore, TaskScheduler}
@history_limit 20
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Syntropy.PubSub, "lattice:events")
end
{:ok,
assign(socket,
agent: nil,
task_history: [],
recommendation_history: [],
snapshot_history: []
)}
end
@impl true
def handle_params(%{"id" => agent_id}, _uri, socket) do
{:noreply, load_agent(socket, agent_id)}
end
@impl true
def handle_info({:lattice_event, _event}, socket) do
agent_id = socket.assigns.agent && socket.assigns.agent.id
{:noreply, load_agent(socket, agent_id)}
end
@impl true
def render(assigns) do
~H"""
<main class="dashboard">
<section class="dashboard-header glass-panel p-4">
<div>
<h1>Agent inspection</h1>
<p class="body-copy">Retained state and recent participation for one agent.</p>
</div>
<nav class="dashboard-nav">
<.link navigate={~p"/"} class="text-link">Lattice</.link>
<span> · </span>
<.link navigate={~p"/tasks"} class="text-link">Tasks</.link>
<span> · </span>
<.link navigate={~p"/history"} class="text-link">History</.link>
</nav>
</section>
<%= if @agent do %>
<section id="agent-summary" class="glass-panel detail-panel mt-4 mb-4">
<header class="section-heading section-heading--flush">
<div>
<h2><%= @agent.name %></h2>
</div>
<div class="task-list-meta">
<%= if @agent.temporary do %>
<span class="status-chip status-chip--warn">temporary</span>
<% end %>
<.link navigate={graph_path(nil, @agent.id)} class="status-chip status-chip--muted">
Focus in live graph
</.link>
</div>
</header>
<div class="outcome-stat-grid">
<div>
<span class="meta-label">ID</span>
<strong><%= @agent.id %></strong>
</div>
<div>
<span class="meta-label">Node</span>
<strong><%= Syntropy.node_id() %></strong>
</div>
<div>
<span class="meta-label">Perspective</span>
<strong><%= @agent.perspective %></strong>
</div>
<div>
<span class="meta-label">Position</span>
<strong><%= format_score(@agent.position) %></strong>
</div>
</div>
</section>
<div class="two-column-grid mb-4">
<article class="glass-panel detail-panel">
<header class="section-heading section-heading--flush">
<div>
<h2>Connections</h2>
</div>
</header>
<%= if map_size(@agent.connections) == 0 do %>
<div class="empty-panel">
<p id="agent-connections-empty" class="body-copy">No retained connections.</p>
</div>
<% else %>
<ul id="agent-connections" class="task-list">
<%= for {target_id, connection} <- Enum.sort_by(@agent.connections, fn {target_id, _} -> target_id end) do %>
<li id={"connection-#{target_id}"} class="task-list-item task-list-item--compact">
<div class="flex-row justify-between">
<strong><%= target_id %></strong>
<span class="meta-copy">
weight <%= format_score(connection.weight) %>
· <%= connection.interactions %> interactions
</span>
</div>
</li>
<% end %>
</ul>
<% end %>
</article>
<article class="glass-panel detail-panel">
<header class="section-heading section-heading--flush">
<div>
<h2>Recent task participation</h2>
</div>
</header>
<%= if @task_history == [] do %>
<div class="empty-panel">
<p id="agent-tasks-empty" class="body-copy">No recent task participation.</p>
</div>
<% else %>
<ul id="agent-task-history" class="task-list">
<%= for entry <- @task_history do %>
<li id={"agent-task-#{entry.task.task_id}"} class="task-list-item task-list-item--compact">
<div class="flex-row justify-between">
<strong><%= entry.task.task_id %></strong>
<span class="meta-copy">node <%= node_id_for(entry.task) %></span>
</div>
<div class="task-list-meta">
<span class="meta-copy"><%= entry.task.strategy %> resolved <%= entry.task.resolved_mode %></span>
<%= if entry.snapshot do %>
<.link navigate={graph_path(entry.snapshot.id, @agent.id)} class="status-chip status-chip--muted">
<%= entry.snapshot.id %>
</.link>
<% end %>
</div>
</li>
<% end %>
</ul>
<% end %>
</article>
</div>
<div class="two-column-grid">
<article class="glass-panel detail-panel">
<header class="section-heading section-heading--flush">
<div>
<h2>Recent recommendation involvement</h2>
</div>
</header>
<%= if @recommendation_history == [] do %>
<div class="empty-panel">
<p id="agent-recommendations-empty" class="body-copy">No recent recommendation involvement.</p>
</div>
<% else %>
<ul id="agent-recommendations" class="task-list">
<%= for recommendation <- @recommendation_history do %>
<li id={"agent-recommendation-#{recommendation.id}"} class="task-list-item task-list-item--compact">
<div class="flex-row justify-between">
<strong><%= recommendation.id %></strong>
<span class="meta-copy">node <%= node_id_for(recommendation) %></span>
</div>
<div class="task-list-meta">
<span class="status-chip status-chip--muted"><%= recommendation.kind %></span>
<span class="status-chip status-chip--warn"><%= recommendation.status %></span>
</div>
</li>
<% end %>
</ul>
<% end %>
</article>
<article class="glass-panel detail-panel">
<header class="section-heading section-heading--flush">
<div>
<h2>Recent snapshots</h2>
</div>
</header>
<%= if @snapshot_history == [] do %>
<div class="empty-panel">
<p id="agent-snapshots-empty" class="body-copy">No recent snapshots for this agent.</p>
</div>
<% else %>
<ul id="agent-snapshots" class="task-list">
<%= for snapshot <- @snapshot_history do %>
<li id={"agent-snapshot-#{snapshot.id}"} class="task-list-item task-list-item--compact">
<div class="flex-row justify-between">
<.link navigate={graph_path(snapshot.id, @agent.id)} class="text-link">
<strong><%= snapshot.id %></strong>
</.link>
<span class="meta-copy">node <%= node_id_for(snapshot) %></span>
</div>
<span class="meta-copy">trigger <%= snapshot.trigger_kind %></span>
</li>
<% end %>
</ul>
<% end %>
</article>
</div>
<% else %>
<section class="glass-panel detail-panel mt-4">
<div class="empty-panel">
<p id="agent-not-found" class="body-copy">Agent not found.</p>
</div>
</section>
<% end %>
</main>
"""
end
defp load_agent(socket, nil),
do:
assign(socket,
agent: nil,
task_history: [],
recommendation_history: [],
snapshot_history: []
)
defp load_agent(socket, agent_id) do
agent = Enum.find(LatticeSupervisor.list_agents(), &(&1.id == agent_id))
assign(socket,
agent: agent,
task_history: recent_tasks_for(agent_id),
recommendation_history: recent_recommendations_for(agent_id),
snapshot_history: recent_snapshots_for(agent_id)
)
end
defp recent_tasks_for(nil), do: []
defp recent_tasks_for(agent_id) do
snapshots_by_task_id =
HistoryStore.recent(@history_limit)
|> Map.new(fn snapshot ->
{snapshot.task_id, snapshot}
end)
TaskScheduler.recent(@history_limit)
|> Enum.filter(fn task ->
agent_id in task.selected_agent_ids or agent_id in task.contributing_agent_ids
end)
|> Enum.map(fn task ->
%{
task: task,
snapshot: Map.get(snapshots_by_task_id, task.task_id)
}
end)
end
defp recent_recommendations_for(nil), do: []
defp recent_recommendations_for(agent_id) do
RecommendationStore.recent(@history_limit)
|> Enum.filter(fn recommendation ->
agent_id in recommendation.candidate_agent_ids or
recommendation.applied_agent_id == agent_id
end)
end
defp recent_snapshots_for(nil), do: []
defp recent_snapshots_for(agent_id) do
HistoryStore.recent(@history_limit)
|> Enum.filter(fn snapshot ->
Enum.any?(snapshot.agents, &(&1.id == agent_id))
end)
end
defp graph_path(snapshot_id, agent_id) do
params =
%{}
|> maybe_put("snapshot", snapshot_id)
|> maybe_put("agent", agent_id)
~p"/?#{params}"
end
defp maybe_put(params, _key, nil), do: params
defp maybe_put(params, key, value), do: Map.put(params, key, value)
end