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/operations_live.ex
defmodule SyntropyWeb.OperationsLive do
@moduledoc """
Mission-control operations: agent deploy/remove, webhook administration,
and runtime provider configuration — the same controls the REST API and
platform cockpit expose, available directly in the LiveView.
"""
use SyntropyWeb, :live_view
import SyntropyWeb.RuntimeViewHelpers
alias Syntropy.{
ClusterInventory,
Contract,
EventRecorder,
LatticeSupervisor,
RuntimeConfig,
TaskScheduler,
Usage
}
alias Syntropy.Webhooks.Registry, as: WebhookRegistry
alias SyntropyWeb.Api.AgentRequest
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Syntropy.PubSub, "lattice:events")
end
{:ok,
socket
|> assign(
perspective_presets: perspective_presets(),
agent_feedback: nil,
webhook_feedback: nil,
new_webhook_secret: nil,
provider_feedback: nil,
provider_test: nil,
limits_feedback: nil
)
|> refresh_operations()}
end
@impl true
def handle_info({:lattice_event, _event}, socket) do
{:noreply, refresh_operations(socket)}
end
@impl true
def handle_event("deploy_agent", %{"agent" => agent_params}, socket) do
request = %{
"name" => Map.get(agent_params, "name"),
"perspective" => Map.get(agent_params, "perspective"),
"knowledge_items" => split_list(Map.get(agent_params, "knowledge_items", ""))
}
with {:ok, parsed} <- AgentRequest.parse(request),
{:ok, agent_id} <-
LatticeSupervisor.add_agent(parsed.name, parsed.perspective, parsed.opts) do
agent = LatticeSupervisor.get_agent(agent_id)
EventRecorder.record("agent_created", %{
agent_id: agent.id,
runtime_id: agent.runtime_id,
node_id: agent.node_id,
agent_name: agent.name,
perspective: agent.perspective,
source: "cockpit"
})
{:noreply,
socket
|> assign(agent_feedback: {:ok, "Deployed #{agent.name} (#{agent.perspective})."})
|> refresh_operations()}
else
{:error, errors} when is_list(errors) ->
{:noreply, assign(socket, agent_feedback: {:error, agent_error_text(errors)})}
{:error, _reason} ->
{:noreply, assign(socket, agent_feedback: {:error, "The agent could not be started."})}
end
end
@impl true
def handle_event("remove_agent", %{"id" => agent_id}, socket) do
case LatticeSupervisor.get_agent(agent_id) do
nil ->
{:noreply, assign(socket, agent_feedback: {:error, "Agent #{agent_id} was not found."})}
agent ->
if agent.runtime_id in TaskScheduler.active_agent_ids() do
{:noreply,
assign(socket,
agent_feedback:
{:error, "Agent #{agent_id} is executing an active run and cannot be removed."}
)}
else
remove_agent(socket, agent)
end
end
end
@impl true
def handle_event("create_webhook", %{"webhook" => webhook_params}, socket) do
attrs = %{
"url" => Map.get(webhook_params, "url"),
"events" => split_list(Map.get(webhook_params, "events", ""))
}
case WebhookRegistry.register(attrs) do
{:ok, webhook} ->
{:noreply,
socket
|> assign(
webhook_feedback: {:ok, "Webhook registered. Copy the signing secret now:"},
new_webhook_secret: webhook.secret
)
|> refresh_operations()}
{:error, errors} ->
{:noreply,
assign(socket,
webhook_feedback: {:error, agent_error_text(errors)},
new_webhook_secret: nil
)}
end
end
@impl true
def handle_event("toggle_webhook", %{"id" => webhook_id, "active" => active}, socket) do
case WebhookRegistry.set_active(webhook_id, active == "true") do
{:ok, webhook} ->
verb = if webhook.active, do: "resumed", else: "paused"
{:noreply,
socket
|> assign(webhook_feedback: {:ok, "Deliveries #{verb} for #{webhook.url}."})
|> refresh_operations()}
{:error, :not_found} ->
{:noreply,
assign(socket, webhook_feedback: {:error, "Webhook #{webhook_id} was not found."})}
end
end
@impl true
def handle_event("delete_webhook", %{"id" => webhook_id}, socket) do
case WebhookRegistry.remove(webhook_id) do
:ok ->
{:noreply,
socket
|> assign(webhook_feedback: {:ok, "Webhook removed."}, new_webhook_secret: nil)
|> refresh_operations()}
{:error, :not_found} ->
{:noreply,
assign(socket, webhook_feedback: {:error, "Webhook #{webhook_id} was not found."})}
end
end
@impl true
def handle_event("apply_provider", %{"provider" => provider_params}, socket) do
case RuntimeConfig.apply_provider(provider_params) do
{:ok, _report} ->
{:noreply,
socket
|> assign(
provider_feedback: {:ok, "Provider configuration applied."},
provider_test: nil
)
|> refresh_operations()}
{:error, errors} ->
{:noreply, assign(socket, provider_feedback: {:error, Enum.join(errors, " ")})}
end
end
@impl true
def handle_event("test_provider", %{"provider" => provider_params}, socket) do
{:noreply, assign(socket, provider_test: RuntimeConfig.test_provider(provider_params))}
end
@impl true
def handle_event("apply_budget", %{"limits" => %{"usage_budget_tokens" => tokens}}, socket) do
case Usage.apply_budget(tokens) do
{:ok, _budget} ->
{:noreply,
socket
|> assign(limits_feedback: {:ok, "Usage budget updated."})
|> refresh_operations()}
{:error, message} ->
{:noreply, assign(socket, limits_feedback: {:error, message})}
end
end
@impl true
def render(assigns) do
~H"""
<main class="dashboard">
<section class="dashboard-header glass-panel p-4">
<div>
<h1>Operations</h1>
<p class="body-copy">Agent deployment, webhooks, and runtime configuration.</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>
<span> · </span>
<.link patch={~p"/operations"} class="text-link">Operations</.link>
</nav>
<div class="status-chip-row">
<span class={chip_class(@runtime_report.provider.provider)}>
Provider: <%= @runtime_report.provider.provider %>
</span>
<span class={chip_class(if(@runtime_report.provider.valid, do: "ready", else: "not_ready"))}>
Config: <%= if @runtime_report.provider.valid, do: "valid", else: "invalid" %>
</span>
<span class={chip_class(@runtime_report.persistence.status)}>
Durability: <%= humanize_status(@runtime_report.persistence.status) %>
</span>
</div>
</section>
<section class="glass-panel detail-panel mt-4">
<header class="section-heading">
<div>
<h2>Agents</h2>
<p class="body-copy">
Deploy and remove lattice agents. Agents executing an active run cannot be removed.
</p>
</div>
</header>
<form id="deploy-agent-form" class="flex-col" phx-submit="deploy_agent">
<div class="flex-row" style="flex-wrap: wrap; gap: 8px;">
<input
type="text"
name="agent[name]"
placeholder="Agent name (e.g. Architecture)"
style="flex: 1; min-width: 180px;"
/>
<select name="agent[perspective]" style="max-width: 240px;">
<%= for preset <- @perspective_presets do %>
<option value={preset.perspective}><%= preset.name %> (<%= preset.perspective %>)</option>
<% end %>
</select>
<input
type="text"
name="agent[knowledge_items]"
placeholder="Knowledge items, comma-separated (optional)"
style="flex: 2; min-width: 220px;"
/>
<button type="submit" class="primary-button">Deploy agent</button>
</div>
</form>
<%= if @agent_feedback do %>
<p id="agent-feedback" class="meta-label" style={feedback_style(@agent_feedback)}>
<%= feedback_text(@agent_feedback) %>
</p>
<% end %>
<%= if @agents == [] do %>
<div class="empty-panel">
<p id="operations-agents-empty" class="body-copy">
No agents are deployed. Deploy your first agent above to start the lattice.
</p>
</div>
<% else %>
<ul id="operations-agent-list" class="task-list">
<%= for agent <- @agents do %>
<li id={"operations-agent-#{agent.id}"} class="task-list-item">
<div class="section-heading section-heading--flush">
<div>
<p class="task-title"><%= agent.name %></p>
<p class="meta-copy">
<%= agent.perspective %>
· position <%= format_score(agent.position) %>
· node <%= node_id_for(agent) %>
<%= if agent.temporary do %>
· temporary
<% end %>
</p>
</div>
<div class="flex-row" style="gap: 8px; align-items: center;">
<%= if busy_agent?(agent, @busy_runtime_ids) do %>
<span class="status-chip status-chip--warn">busy</span>
<% end %>
<button
type="button"
class="ds-button--destructive"
phx-click="remove_agent"
phx-value-id={agent.id}
data-confirm={"Remove agent #{agent.name}? Its knowledge is redistributed to connected agents."}
disabled={busy_agent?(agent, @busy_runtime_ids)}
>
Remove
</button>
</div>
</div>
</li>
<% end %>
</ul>
<% end %>
</section>
<section class="glass-panel detail-panel mt-4">
<header class="section-heading">
<div>
<h2>Webhooks</h2>
<p class="body-copy">
Signed runtime-event deliveries. Use <code>*</code> to subscribe to every event.
</p>
</div>
</header>
<form id="create-webhook-form" class="flex-col" phx-submit="create_webhook">
<div class="flex-row" style="flex-wrap: wrap; gap: 8px;">
<input
type="url"
name="webhook[url]"
placeholder="https://example.com/syntropy-events"
style="flex: 2; min-width: 240px;"
/>
<input
type="text"
name="webhook[events]"
placeholder="Events, comma-separated (e.g. task_completed) or *"
style="flex: 2; min-width: 240px;"
/>
<button type="submit" class="primary-button">Register webhook</button>
</div>
</form>
<%= if @webhook_feedback do %>
<p id="webhook-feedback" class="meta-label" style={feedback_style(@webhook_feedback)}>
<%= feedback_text(@webhook_feedback) %>
</p>
<% end %>
<%= if @new_webhook_secret do %>
<p id="webhook-secret" class="body-copy" style="word-break: break-all;">
<code><%= @new_webhook_secret %></code>
— shown once; deliveries are signed with this secret.
</p>
<% end %>
<%= if @webhooks == [] do %>
<div class="empty-panel">
<p id="operations-webhooks-empty" class="body-copy">
No webhooks registered. Register one to receive signed runtime events.
</p>
</div>
<% else %>
<ul id="operations-webhook-list" class="task-list">
<%= for {webhook, deliveries} <- @webhooks do %>
<li id={"operations-webhook-#{webhook.id}"} class="task-list-item">
<div class="section-heading section-heading--flush">
<div>
<p class="task-title" style="word-break: break-all;"><%= webhook.url %></p>
<p class="meta-copy">
events: <%= Enum.join(webhook.events, ", ") %>
· <%= length(deliveries) %> recent deliveries
</p>
</div>
<div class="flex-row" style="gap: 8px; align-items: center;">
<span class={chip_class(if(webhook.active, do: "ok", else: "disabled"))}>
<%= if webhook.active, do: "active", else: "paused" %>
</span>
<button
type="button"
phx-click="toggle_webhook"
phx-value-id={webhook.id}
phx-value-active={to_string(not webhook.active)}
>
<%= if webhook.active, do: "Pause", else: "Resume" %>
</button>
<button
type="button"
class="ds-button--destructive"
phx-click="delete_webhook"
phx-value-id={webhook.id}
data-confirm={"Remove webhook #{webhook.url}? Deliveries stop immediately."}
>
Remove
</button>
</div>
</div>
</li>
<% end %>
</ul>
<% end %>
</section>
<section class="glass-panel detail-panel mt-4">
<header class="section-heading">
<div>
<h2>Runtime configuration</h2>
<p class="body-copy">
Applied changes take effect immediately for subsequent LLM calls.
Scope: <%= @runtime_report.provider.applied_scope %>.
</p>
</div>
</header>
<div class="four-column-grid mb-4">
<article class="run-context-card">
<span class="meta-label">Provider</span>
<strong><%= @runtime_report.provider.provider %></strong>
</article>
<article class="run-context-card">
<span class="meta-label">Model</span>
<strong><%= @runtime_report.provider.model || "—" %></strong>
</article>
<article class="run-context-card">
<span class="meta-label">Base URL</span>
<strong style="word-break: break-all;"><%= @runtime_report.provider.base_url || "—" %></strong>
</article>
<article class="run-context-card">
<span class="meta-label">API key</span>
<strong><%= if @runtime_report.provider.api_key_configured, do: "configured", else: "not set" %></strong>
</article>
</div>
<%= if @runtime_report.provider.validation_errors != [] do %>
<p id="provider-validation-errors" class="meta-label text-danger">
<%= Enum.join(@runtime_report.provider.validation_errors, " ") %>
</p>
<% end %>
<form id="provider-form" class="flex-col" phx-submit="apply_provider">
<div class="flex-row" style="flex-wrap: wrap; gap: 8px;">
<select name="provider[provider]" style="max-width: 160px;">
<%= for provider <- ~w(mock ollama openai) do %>
<option value={provider} selected={@runtime_report.provider.provider == provider}>
<%= provider %>
</option>
<% end %>
</select>
<input
type="text"
name="provider[model]"
placeholder="Model (e.g. llama3.1:8b)"
value={@runtime_report.provider.model}
style="flex: 1; min-width: 180px;"
/>
<input
type="text"
name="provider[base_url]"
placeholder="Base URL"
value={@runtime_report.provider.base_url}
style="flex: 2; min-width: 220px;"
/>
<input
type="password"
name="provider[api_key]"
placeholder="API key (leave blank to keep)"
style="flex: 1; min-width: 180px;"
/>
</div>
<div class="flex-row" style="gap: 8px;">
<button type="submit" class="primary-button">Apply provider</button>
</div>
</form>
<form id="provider-test-form" phx-submit="test_provider" class="mt-2">
<input type="hidden" name="provider[_probe]" value="current" />
<button type="submit">Test current provider</button>
</form>
<%= if @provider_feedback do %>
<p id="provider-feedback" class="meta-label" style={feedback_style(@provider_feedback)}>
<%= feedback_text(@provider_feedback) %>
</p>
<% end %>
<%= if @provider_test do %>
<p id="provider-test-result" class="meta-label">
Probe: <%= @provider_test.status %>
· <%= @provider_test.provider %>
<%= if @provider_test.model do %>
· <%= @provider_test.model %>
<% end %>
</p>
<% end %>
<details class="mt-4">
<summary class="meta-label" style="cursor: pointer;">
Env export for a permanent configuration
</summary>
<pre style="margin-top: 8px;"><%= @runtime_report.provider.env_export %></pre>
</details>
<div class="mt-4" style="border-top: 1px solid var(--ds-border-subtle); padding-top: 16px;">
<h3 class="meta-label">Fair-use budget</h3>
<p class="body-copy">
<%= budget_summary(@runtime_report.limits.usage_budget) %>
</p>
<form id="limits-form" class="flex-row mt-2" phx-submit="apply_budget" style="gap: 8px;">
<input
type="number"
name="limits[usage_budget_tokens]"
min="0"
placeholder="Token budget (0 disables the cap)"
value={@runtime_report.limits.usage_budget.limit_tokens}
style="max-width: 260px;"
/>
<button type="submit">Update budget</button>
</form>
<%= if @limits_feedback do %>
<p id="limits-feedback" class="meta-label" style={feedback_style(@limits_feedback)}>
<%= feedback_text(@limits_feedback) %>
</p>
<% end %>
</div>
</section>
</main>
"""
end
defp refresh_operations(socket) do
assign(socket,
agents: ClusterInventory.cluster_agents(),
busy_runtime_ids: ClusterInventory.busy_runtime_ids(TaskScheduler.active_tasks_snapshot()),
webhooks: WebhookRegistry.list(),
runtime_report: RuntimeConfig.report()
)
end
defp remove_agent(socket, agent) do
case LatticeSupervisor.remove_agent(agent.id) do
:ok ->
EventRecorder.record("agent_removed", %{
agent_id: agent.id,
runtime_id: agent.runtime_id,
node_id: agent.node_id,
agent_name: agent.name,
perspective: agent.perspective,
source: "cockpit"
})
{:noreply,
socket
|> assign(agent_feedback: {:ok, "Removed #{agent.name}."})
|> refresh_operations()}
{:error, :not_found} ->
{:noreply, assign(socket, agent_feedback: {:error, "Agent #{agent.id} was not found."})}
end
end
defp busy_agent?(agent, busy_runtime_ids) do
runtime_id = Map.get(agent, :runtime_id) || agent.id
MapSet.member?(busy_runtime_ids, runtime_id)
end
defp perspective_presets do
Contract.perspectives_catalog()
|> Map.get("presets", [])
|> Enum.map(fn preset ->
%{name: Map.fetch!(preset, "name"), perspective: Map.fetch!(preset, "perspective")}
end)
end
defp split_list(value) when is_binary(value) do
value
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
defp split_list(_value), do: []
defp agent_error_text(errors) do
Enum.map_join(errors, " ", fn
%{field: field, message: message} -> "#{field} #{message}."
%{"field" => field, "message" => message} -> "#{field} #{message}."
other -> to_string(other)
end)
end
defp feedback_style({:ok, _message}), do: "color: var(--ds-success);"
defp feedback_style({:error, _message}), do: "color: var(--ds-danger);"
defp feedback_text({_tone, message}), do: message
defp budget_summary(%{limit_tokens: 0}) do
"No token budget is set. Runs are uncapped."
end
defp budget_summary(budget) do
used = Map.get(budget, :used_tokens, 0)
limit = Map.get(budget, :limit_tokens, 0)
"#{used} of #{limit} tokens used in the rolling 30-day window."
end
end