Packages

A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.

Current section

Files

Jump to
syntropy lib syntropy task_scheduler run_envelope.ex
Raw

lib/syntropy/task_scheduler/run_envelope.ex

defmodule Syntropy.TaskScheduler.RunEnvelope do
@moduledoc """
Run envelope builders and envelope collection helpers.
Every run state — active, completed, failed, cancelled — is projected into
the same envelope shape consumed by the runs API, API clients, and the
persistence write-through. Builders are pure over their inputs apart from
reading usage summaries and history snapshots for the run.
"""
alias Syntropy.{ClusterInfo, HistoryStore, OutputQuality}
alias Syntropy.LLMClient.UsageRecorder
alias Syntropy.TaskScheduler.{SourceInquiry, Support}
@type run_envelope :: Syntropy.TaskScheduler.run_envelope()
@doc """
Envelope for a run that is still executing.
"""
@spec active(map()) :: run_envelope()
def active(task_info) do
%{
id: task_info.task_id,
status: "running",
kind: Map.get(task_info, :kind, "prompt"),
node_id: task_info.node_id,
coordinator_node_id: task_info.coordinator_node_id,
prompt: task_info.prompt,
strategy: task_info.strategy,
resolved_mode: task_info.resolved_mode,
selected_agent_ids: task_info.selected_agent_ids,
selected_agents: Enum.map(task_info.selected_agents, &Support.agent_ref/1),
contributing_agent_ids: [],
contributing_agents: [],
thoughts: [],
thought_count: 0,
agent_attempts: active_agent_attempts(task_info.selected_agents, task_info.started_at),
execution_quality:
execution_quality_for_counts("running", length(task_info.selected_agents), 0, 0),
provider_profile: Map.get(task_info, :provider_profile),
llm_usage: UsageRecorder.summary(task_info.task_id),
synthesis: "",
structured_answer: nil,
output_quality: nil,
selection_trace: task_info.selection_trace,
recommendation_ids: [],
snapshot_ids: [],
source_review: Map.get(task_info, :source_review),
citation_audit: nil,
inquiry_rounds: [],
tool_attempts: [],
expanded_evidence: [],
inquiry_quality:
SourceInquiry.active_inquiry_quality(
Map.get(task_info, :source_review),
Map.get(task_info, :source_tool)
),
receipts: [],
code_review: Map.get(task_info, :code_review),
error: nil,
started_at: task_info.started_at,
updated_at: task_info.updated_at,
completed_at: nil,
failed_at: nil
}
end
@doc """
Envelope for a completed task result.
"""
@spec completed(map()) :: run_envelope()
def completed(task) do
quality_fields = completed_quality_fields(task)
%{
id: task.task_id,
status: "completed",
kind: Map.get(task, :kind, "prompt"),
node_id: task.node_id,
coordinator_node_id: Map.get(task, :coordinator_node_id, Map.get(task, :node_id)),
prompt: task.prompt,
strategy: task.strategy,
resolved_mode: task.resolved_mode,
selected_agent_ids: task.selected_agent_ids,
selected_agents: Map.get(task, :selected_agents, []),
contributing_agent_ids: task.contributing_agent_ids,
contributing_agents: Map.get(task, :contributing_agents, []),
thoughts: task.thoughts,
thought_count: task.thought_count,
agent_attempts: Map.get(task, :agent_attempts, []),
execution_quality:
Map.get(
task,
:execution_quality,
execution_quality_for_counts(
"complete",
length(Map.get(task, :selected_agents, [])),
length(Map.get(task, :contributing_agents, [])),
0
)
),
provider_profile: Map.get(task, :provider_profile),
llm_usage: Map.get(task, :llm_usage) || UsageRecorder.summary(task.task_id),
synthesis: task.synthesis,
structured_answer: Map.get(task, :structured_answer) || quality_fields.structured_answer,
output_quality: Map.get(task, :output_quality) || quality_fields.output_quality,
selection_trace: task.selection_trace,
recommendation_ids: task.recommendation_ids,
snapshot_ids: snapshot_ids_for_task(task.task_id),
source_review: Map.get(task, :source_review),
citation_audit: Map.get(task, :citation_audit),
inquiry_rounds: Map.get(task, :inquiry_rounds, []),
tool_attempts: Map.get(task, :tool_attempts, []),
expanded_evidence: Map.get(task, :expanded_evidence, []),
inquiry_quality: Map.get(task, :inquiry_quality),
receipts: Map.get(task, :receipts, []),
code_review: Map.get(task, :code_review),
error: nil,
started_at: Map.get(task, :started_at),
updated_at: Map.get(task, :updated_at) || Map.get(task, :completed_at),
completed_at: Map.get(task, :completed_at),
failed_at: nil
}
end
@doc """
Envelope for a run that failed with `reason`.
"""
@spec failed(map(), term()) :: run_envelope()
def failed(task_info, reason) do
failed_at = DateTime.utc_now()
%{
id: task_info.task_id,
status: "failed",
kind: Map.get(task_info, :kind, "prompt"),
node_id: task_info.node_id,
coordinator_node_id: task_info.coordinator_node_id,
prompt: task_info.prompt,
strategy: task_info.strategy,
resolved_mode: Map.get(task_info, :resolved_mode),
selected_agent_ids: Map.get(task_info, :selected_agent_ids, []),
selected_agents:
task_info |> Map.get(:selected_agents, []) |> Enum.map(&Support.agent_ref/1),
contributing_agent_ids: [],
contributing_agents: [],
thoughts: [],
thought_count: 0,
agent_attempts:
Map.get(task_info, :agent_attempts) ||
failed_attempts_from_reason(reason) ||
failed_agent_attempts(Map.get(task_info, :selected_agents, []), failed_at, reason),
execution_quality:
execution_quality_from_reason(reason) ||
execution_quality_for_counts(
"failed",
length(Map.get(task_info, :selected_agents, [])),
0,
length(Map.get(task_info, :selected_agents, []))
),
provider_profile: Map.get(task_info, :provider_profile),
llm_usage: UsageRecorder.summary(task_info.task_id),
synthesis: "",
structured_answer: nil,
output_quality: OutputQuality.failed(reason),
selection_trace: Map.get(task_info, :selection_trace, []),
recommendation_ids: [],
snapshot_ids: [],
source_review: Map.get(task_info, :source_review),
citation_audit: nil,
inquiry_rounds: Map.get(task_info, :inquiry_rounds, []),
tool_attempts: Map.get(task_info, :tool_attempts, []),
expanded_evidence: Map.get(task_info, :expanded_evidence, []),
inquiry_quality:
Map.get(task_info, :inquiry_quality) ||
SourceInquiry.failed_inquiry_quality(
Map.get(task_info, :source_review),
Map.get(task_info, :source_tool)
),
receipts: Map.get(task_info, :receipts, []),
code_review: Map.get(task_info, :code_review),
error: run_error(reason),
started_at: Map.get(task_info, :started_at),
updated_at: failed_at,
completed_at: nil,
failed_at: failed_at
}
end
@doc """
Envelope for an operator-cancelled run.
"""
@spec cancelled(map()) :: run_envelope()
def cancelled(task_info) do
selected_count = task_info |> Map.get(:selected_agents, []) |> length()
envelope = failed(task_info, :cancelled)
%{
envelope
| status: "cancelled",
execution_quality: execution_quality_for_counts("cancelled", selected_count, 0, 0),
agent_attempts: Enum.map(envelope.agent_attempts, &Map.put(&1, :status, "cancelled"))
}
end
@doc """
Public projection of an internal agent attempt.
"""
@spec public_agent_attempt(map()) :: Syntropy.TaskScheduler.agent_attempt()
def public_agent_attempt(attempt) do
attempt.agent
|> base_agent_attempt()
|> Map.merge(%{
status: attempt.status,
contributed: attempt.contributed,
started_at: attempt.started_at,
completed_at: attempt.completed_at,
failed_at: attempt.failed_at,
duration_ms: attempt.duration_ms,
error: attempt.error
})
end
@doc """
Execution quality summary derived from agent attempts.
"""
@spec execution_quality([map()]) :: Syntropy.TaskScheduler.execution_quality()
def execution_quality(attempts) do
selected_count = length(attempts)
contributing_count = Enum.count(attempts, & &1.contributed)
failed_count = selected_count - contributing_count
status =
cond do
contributing_count == 0 -> "failed"
failed_count > 0 -> "partial"
true -> "complete"
end
execution_quality_for_counts(status, selected_count, contributing_count, failed_count)
end
@doc """
Removes duplicate envelopes by id, keeping first occurrence order.
"""
@spec dedupe([run_envelope()]) :: [run_envelope()]
def dedupe(runs) do
runs
|> Enum.reduce({MapSet.new(), []}, fn run, {seen, acc} ->
if MapSet.member?(seen, run.id) do
{seen, acc}
else
{MapSet.put(seen, run.id), [run | acc]}
end
end)
|> elem(1)
|> Enum.reverse()
end
@doc """
Inserts or replaces `run` at the head of `runs`, bounded to `limit`.
"""
@spec upsert([run_envelope()], run_envelope(), pos_integer()) :: [run_envelope()]
def upsert(runs, run, limit) do
[run | Enum.reject(runs, &(&1.id == run.id))]
|> Enum.take(limit)
end
@doc """
Sort key for run recency ordering.
"""
@spec sort_key(run_envelope()) :: integer()
def sort_key(%{updated_at: %DateTime{} = updated_at}),
do: DateTime.to_unix(updated_at, :microsecond)
def sort_key(%{started_at: %DateTime{} = started_at}),
do: DateTime.to_unix(started_at, :microsecond)
def sort_key(_run), do: 0
defp completed_quality_fields(task) do
OutputQuality.assess(Map.get(task, :synthesis, ""),
source_review: Map.get(task, :source_review),
citation_audit: Map.get(task, :citation_audit),
execution_quality: Map.get(task, :execution_quality),
inquiry_quality: Map.get(task, :inquiry_quality)
)
end
defp active_agent_attempts(selected_agents, started_at) do
Enum.map(selected_agents, fn agent ->
agent
|> base_agent_attempt()
|> Map.merge(%{
status: "selected",
contributed: false,
started_at: started_at,
completed_at: nil,
failed_at: nil,
duration_ms: nil,
error: nil
})
end)
end
defp failed_agent_attempts([], _failed_at, _reason), do: []
defp failed_agent_attempts(selected_agents, failed_at, reason) do
failure = run_error(reason)
Enum.map(selected_agents, fn agent ->
agent
|> base_agent_attempt()
|> Map.merge(%{
status: "failed",
contributed: false,
started_at: nil,
completed_at: nil,
failed_at: failed_at,
duration_ms: nil,
error: failure
})
end)
end
defp failed_attempts_from_reason({:no_successful_thoughts, attempts}) when is_list(attempts) do
Enum.map(attempts, &public_agent_attempt/1)
end
defp failed_attempts_from_reason(_reason), do: nil
defp execution_quality_from_reason({:no_successful_thoughts, attempts})
when is_list(attempts) do
execution_quality(attempts)
end
defp execution_quality_from_reason(_reason), do: nil
defp base_agent_attempt(agent) do
%{
agent_id: Map.get(agent, :id),
runtime_id: Map.get(agent, :runtime_id, Map.get(agent, :id)),
node_id: Map.get(agent, :node_id, ClusterInfo.node_id()),
agent_name: Map.get(agent, :name, Map.get(agent, :id, "agent")),
perspective: Map.get(agent, :perspective, "unknown")
}
end
defp execution_quality_for_counts(status, selected_count, contributing_count, failed_count) do
warnings =
[]
|> maybe_execution_warning(
status == "partial",
"#{contributing_count}/#{selected_count} selected agents contributed; inspect failed attempts before trusting the synthesis."
)
|> maybe_execution_warning(
status == "failed" and selected_count > 0,
"No selected agents contributed a usable result."
)
%{
status: status,
selected_count: selected_count,
contributing_count: contributing_count,
failed_count: failed_count,
warnings: warnings
}
end
defp maybe_execution_warning(warnings, true, warning), do: warnings ++ [warning]
defp maybe_execution_warning(warnings, false, _warning), do: warnings
defp snapshot_ids_for_task(task_id) do
task_id
|> HistoryStore.for_task()
|> Enum.map(& &1.id)
end
defp run_error(:no_agents), do: %{code: "no_agents", message: "No agents were available."}
defp run_error(:no_successful_thoughts),
do: %{code: "no_successful_thoughts", message: "No agents completed the task."}
defp run_error({:no_successful_thoughts, attempts}) when is_list(attempts) do
%{
code: "no_successful_thoughts",
message: "No selected agents completed the task.",
failures:
Enum.map(attempts, fn attempt ->
%{
agent_id: Map.get(attempt.agent, :id),
runtime_id: Map.get(attempt.agent, :runtime_id, Map.get(attempt.agent, :id)),
perspective: Map.get(attempt.agent, :perspective),
reason: attempt.error
}
end)
}
end
defp run_error({:timeout, {Task.Supervised, :stream, [timeout_ms]}}) do
%{
code: "provider_timeout",
message: "The model provider timed out before the run completed.",
timeout_ms: timeout_ms
}
end
defp run_error(:cancelled) do
%{
code: "cancelled",
message: "The run was cancelled by an operator before it completed."
}
end
defp run_error(reason) do
%{
code: "task_failed",
message: "Task execution failed.",
reason: inspect(reason)
}
end
end