Packages
A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.
Current section
Files
Jump to
Current section
Files
lib/syntropy_web/controllers/api/run_request.ex
defmodule SyntropyWeb.Api.RunRequest do
@moduledoc false
@type parsed_request :: %{prompt: String.t(), opts: keyword()}
@strategies ~w(single parallel all auto)
@required_source_fields ~w(engine repository review_goal)
@source_tool_actions ~w(search open_file related_files dependency_context)
@spec parse(map()) :: {:ok, parsed_request()} | {:error, [map()]}
def parse(params) do
with {:ok, prompt} <- parse_prompt(Map.get(params, "prompt")),
{:ok, strategy} <- parse_strategy(Map.get(params, "strategy")),
{:ok, agent_ids} <- parse_agent_ids(Map.get(params, "agent_ids")),
{:ok, agent_count} <-
parse_positive_integer(Map.get(params, "agent_count"), 3, "agent_count"),
{:ok, synthesize} <- parse_boolean(Map.get(params, "synthesize"), true, "synthesize"),
{:ok, source_review} <- parse_source_review(Map.get(params, "source_review")),
{:ok, source_tool} <- parse_source_tool(Map.get(params, "source_tool")),
:ok <- validate_source_tool_context(source_tool, source_review),
{:ok, code_review} <- parse_code_review(Map.get(params, "code_review")),
{:ok, kind} <- parse_kind(Map.get(params, "kind"), source_review),
:ok <- validate_code_review_context(kind, code_review, source_review, source_tool) do
{:ok,
%{
prompt: prompt,
opts:
[]
|> Keyword.put(:strategy, strategy)
|> maybe_put(:agent_ids, agent_ids)
|> Keyword.put(:agent_count, agent_count)
|> Keyword.put(:synthesize, synthesize)
|> Keyword.put(:kind, kind)
|> maybe_put(:source_review, source_review)
|> maybe_put(:source_tool, source_tool)
|> maybe_put(:code_review, code_review)
}}
else
{:error, errors} -> {:error, errors}
end
end
defp maybe_put(opts, _key, nil), do: opts
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
defp parse_prompt(prompt) when is_binary(prompt) do
trimmed = String.trim(prompt)
if trimmed == "" do
{:error, [error("prompt", "must be a non-empty string")]}
else
{:ok, trimmed}
end
end
defp parse_prompt(_prompt), do: {:error, [error("prompt", "is required")]}
defp parse_strategy(nil), do: {:ok, :parallel}
defp parse_strategy(strategy) when is_binary(strategy) do
if strategy in @strategies do
{:ok, String.to_atom(strategy)}
else
{:error, [error("strategy", "must be one of single, parallel, all, auto")]}
end
end
defp parse_strategy(_strategy),
do: {:error, [error("strategy", "must be one of single, parallel, all, auto")]}
defp parse_agent_ids(nil), do: {:ok, nil}
defp parse_agent_ids(agent_ids) when is_list(agent_ids) do
if Enum.all?(agent_ids, &is_binary/1) do
{:ok, agent_ids}
else
{:error, [error("agent_ids", "must be an array of strings")]}
end
end
defp parse_agent_ids(_agent_ids),
do: {:error, [error("agent_ids", "must be an array of strings")]}
defp parse_positive_integer(nil, default, _field), do: {:ok, default}
defp parse_positive_integer(value, _default, _field) when is_integer(value) and value > 0,
do: {:ok, value}
defp parse_positive_integer(value, _default, field) when is_binary(value) do
case Integer.parse(value) do
{parsed, ""} when parsed > 0 -> {:ok, parsed}
_other -> {:error, [error(field, "must be a positive integer")]}
end
end
defp parse_positive_integer(_value, _default, field),
do: {:error, [error(field, "must be a positive integer")]}
defp parse_boolean(nil, default, _field), do: {:ok, default}
defp parse_boolean(value, _default, _field) when is_boolean(value), do: {:ok, value}
defp parse_boolean(_value, _default, field), do: {:error, [error(field, "must be a boolean")]}
defp parse_kind(nil, nil), do: {:ok, "prompt"}
defp parse_kind(nil, _source_review), do: {:ok, "source_review"}
defp parse_kind(kind, _source_review) when kind in ["prompt", "source_review", "code_review"],
do: {:ok, kind}
defp parse_kind(_kind, _source_review),
do: {:error, [error("kind", "must be prompt, source_review, or code_review")]}
# Receipts runs read a local repository, so they sit behind the same gate
# as the local source tool. `code_review` is never inferred: the kind must
# be explicit and the payload must travel with it, exclusively.
defp parse_code_review(nil), do: {:ok, nil}
defp parse_code_review(code_review) when is_map(code_review) do
checks = Map.get(code_review, "checks")
cond do
not source_tool_enabled?() ->
{:error, [error("code_review", "is disabled in this environment")]}
not present_string?(Map.get(code_review, "repo_path")) ->
{:error, [error("code_review.repo_path", "is required")]}
not present_string?(Map.get(code_review, "base")) ->
{:error, [error("code_review.base", "is required")]}
not (is_nil(checks) or (is_list(checks) and Enum.all?(checks, &present_string?/1))) ->
{:error, [error("code_review.checks", "must be an array of check names")]}
true ->
{:ok, normalize_code_review(code_review, checks)}
end
end
defp parse_code_review(_code_review),
do: {:error, [error("code_review", "must be an object")]}
defp validate_code_review_context("code_review", nil, _source_review, _source_tool),
do: {:error, [error("code_review", "is required when kind is code_review")]}
defp validate_code_review_context("code_review", _code_review, source_review, source_tool)
when not is_nil(source_review) or not is_nil(source_tool),
do:
{:error, [error("code_review", "cannot be combined with source_review or source_tool")]}
defp validate_code_review_context("code_review", _code_review, _source_review, _source_tool),
do: :ok
defp validate_code_review_context(_kind, nil, _source_review, _source_tool), do: :ok
defp validate_code_review_context(_kind, _code_review, _source_review, _source_tool),
do: {:error, [error("kind", "must be code_review when a code_review object is provided")]}
defp normalize_code_review(code_review, checks) do
normalized = %{
"repo_path" => String.trim(Map.get(code_review, "repo_path")),
"base" => String.trim(Map.get(code_review, "base"))
}
normalized =
case Map.get(code_review, "head") do
head when is_binary(head) -> Map.put(normalized, "head", String.trim(head))
_other -> normalized
end
if is_list(checks) do
Map.put(normalized, "checks", Enum.map(checks, &String.trim/1))
else
normalized
end
end
defp parse_source_review(nil), do: {:ok, nil}
defp parse_source_review(source_review) when is_map(source_review) do
missing =
Enum.filter(@required_source_fields, fn field ->
not present_string?(Map.get(source_review, field))
end)
focus_areas = Map.get(source_review, "focus_areas")
cond do
missing != [] ->
{:error,
Enum.map(missing, fn field -> error("source_review.#{field}", "is required") end)}
not is_list(focus_areas) ->
{:error, [error("source_review.focus_areas", "must be an array")]}
true ->
{:ok, source_review}
end
end
defp parse_source_review(_source_review),
do: {:error, [error("source_review", "must be an object")]}
defp parse_source_tool(nil), do: {:ok, nil}
defp parse_source_tool(source_tool) when is_map(source_tool) do
cond do
not source_tool_enabled?() ->
{:error, [error("source_tool", "is disabled in this environment")]}
Map.get(source_tool, "engine") != "librarian" ->
{:error, [error("source_tool.engine", "must be librarian")]}
not present_string?(Map.get(source_tool, "repo_path")) ->
{:error, [error("source_tool.repo_path", "is required")]}
true ->
with {:ok, allowed_actions} <-
parse_source_tool_actions(Map.get(source_tool, "allowed_actions")) do
{:ok, normalize_source_tool(source_tool, allowed_actions)}
end
end
end
defp parse_source_tool(_source_tool),
do: {:error, [error("source_tool", "must be an object")]}
defp validate_source_tool_context(nil, _source_review), do: :ok
defp validate_source_tool_context(_source_tool, source_review) when is_map(source_review) do
if Map.get(source_review, "engine") == "librarian" do
:ok
else
{:error, [error("source_tool", "requires a librarian source_review")]}
end
end
defp validate_source_tool_context(_source_tool, _source_review),
do: {:error, [error("source_tool", "requires source_review")]}
defp source_tool_enabled? do
Application.get_env(:syntropy, :source_tool_enabled, false) ||
System.get_env("SYNTROPY_ENABLE_LOCAL_REPO_REVIEW") == "true"
end
defp normalize_source_tool(source_tool, allowed_actions) do
%{
"engine" => "librarian",
"repo_path" => String.trim(Map.get(source_tool, "repo_path")),
"allowed_actions" => allowed_actions
}
|> maybe_put_source_tool_integer(source_tool, "budget")
|> maybe_put_source_tool_integer(source_tool, "timeout_ms")
|> maybe_put_source_tool_integer(source_tool, "max_stdout_bytes")
end
defp parse_source_tool_actions(nil), do: {:ok, @source_tool_actions}
defp parse_source_tool_actions(actions) when is_list(actions) do
normalized =
actions
|> Enum.map(fn
action when is_binary(action) -> String.trim(action)
_action -> nil
end)
unsupported =
normalized
|> Enum.reject(&(&1 in @source_tool_actions))
cond do
normalized == [] ->
{:error, [error("source_tool.allowed_actions", "must include at least one action")]}
unsupported != [] ->
{:error,
[
error(
"source_tool.allowed_actions",
"must contain only search, open_file, related_files, or dependency_context"
)
]}
true ->
{:ok, Enum.uniq(normalized)}
end
end
defp parse_source_tool_actions(_actions),
do: {:error, [error("source_tool.allowed_actions", "must be an array of strings")]}
defp maybe_put_source_tool_integer(normalized, source_tool, key) do
case positive_integer_value(Map.get(source_tool, key)) do
nil -> normalized
value -> Map.put(normalized, key, value)
end
end
defp positive_integer_value(value) when is_integer(value) and value > 0, do: value
defp positive_integer_value(value) when is_binary(value) do
case Integer.parse(value) do
{parsed, ""} when parsed > 0 -> parsed
_other -> nil
end
end
defp positive_integer_value(_value), do: nil
defp present_string?(value), do: is_binary(value) and String.trim(value) != ""
defp error(field, message), do: %{field: field, message: message}
end