Packages

fnord

0.9.31

AI code archaeology

Current section

Files

Jump to
fnord lib skills runtime.ex
Raw

lib/skills/runtime.ex

defmodule Skills.Runtime do
@moduledoc """
Runtime helpers for executing skills.
This module owns the glue between skill definitions (TOML) and the runtime
components used to execute them:
- model preset parsing (skill `model` string -> `AI.Model.t()`)
- tool tag mapping (skill `tools` -> `AI.Tools.toolbox()`)
- response_format validation
Keeping these helpers in one place avoids duplicating the execution rules
between the agent (`AI.Agent.Skill`) and the tool entry points.
"""
@type model_error :: {:unknown_model_preset, String.t()}
@type tool_tag :: String.t()
@type toolbox_error ::
{:unknown_tool_tag, tool_tag}
| {:missing_basic_tool_tag, [tool_tag]}
@type response_format_error ::
{:invalid_response_format, term()}
| {:missing_response_format_type, map()}
@doc """
Resolve a model preset string (from skill TOML) into an `AI.Model` struct.
Supported values:
- `smart`
- `balanced`
- `fast`
- `web`
- `large_context`
- `large_context:<speed>` where speed is `smart|balanced|fast`
The plain `large_context` form preserves the default behavior by calling
`AI.Model.large_context/0`.
"""
@spec model_from_string(String.t()) :: {:ok, AI.Model.t()} | {:error, model_error}
def model_from_string(model) when is_binary(model) do
case String.split(model, ":", parts: 2) do
["smart"] -> {:ok, AI.Model.smart()}
["balanced"] -> {:ok, AI.Model.balanced()}
["fast"] -> {:ok, AI.Model.fast()}
["web"] -> {:ok, AI.Model.web_search()}
["large_context"] -> {:ok, AI.Model.large_context()}
["large_context", speed] -> large_context_with_speed(speed)
_ -> {:error, {:unknown_model_preset, model}}
end
end
defp large_context_with_speed(speed) do
case speed do
"smart" -> {:ok, AI.Model.large_context(:smart)}
"balanced" -> {:ok, AI.Model.large_context(:balanced)}
"fast" -> {:ok, AI.Model.large_context(:fast)}
_ -> {:error, {:unknown_model_preset, "large_context:#{speed}"}}
end
end
@doc """
Build a toolbox from skill tool tags.
Tags are mapped to `AI.Tools.with_*` groupers. Toolbox construction is
deterministic and ignores input order.
The `basic` tag is required; it acts as the toolbox entrypoint.
"""
@spec toolbox_from_tags([tool_tag]) :: {:ok, AI.Tools.toolbox()} | {:error, toolbox_error}
def toolbox_from_tags(tags) when is_list(tags) do
tags =
tags
|> Enum.filter(&is_binary/1)
|> Enum.uniq()
allowed_tags = AI.Tools.skill_tool_tags()
case Enum.find(tags, fn tag -> not Enum.member?(allowed_tags, tag) end) do
unknown_tag when is_binary(unknown_tag) ->
{:error, {:unknown_tool_tag, unknown_tag}}
nil ->
if Enum.member?(tags, "basic") do
tags
|> build_toolbox_from_tags()
|> then(&{:ok, &1})
else
{:error, {:missing_basic_tool_tag, tags}}
end
end
end
defp build_toolbox_from_tags(tags) do
base = AI.Tools.basic_tools()
# Apply tags in a stable order.
Enum.reduce(stable_tag_order(), base, fn tag, toolbox ->
if Enum.member?(tags, tag) do
apply_tool_tag(tag, toolbox)
else
toolbox
end
end)
end
defp stable_tag_order(), do: AI.Tools.stable_skill_tool_tag_order()
defp apply_tool_tag(tag, toolbox) do
case tag do
"mcp" -> AI.Tools.with_mcps(toolbox)
"frobs" -> AI.Tools.with_frobs(toolbox)
"task" -> AI.Tools.with_task_tools(toolbox)
"coding" -> AI.Tools.with_coding_tools(toolbox)
"web" -> AI.Tools.with_web_tools(toolbox)
"ui" -> AI.Tools.with_ui(toolbox)
"rw" -> AI.Tools.with_rw_tools(toolbox)
"skills" -> AI.Tools.with_skills(toolbox)
_ -> raise "Unknown tool tag reached apply_tool_tag unexpectedly: #{tag}"
end
end
@doc """
Validate a response_format value from a skill.
`nil` is allowed and means default text responses.
When present, the response format must be a map and should include a `type`
key.
"""
@spec validate_response_format(nil | map()) ::
{:ok, nil | map()} | {:error, response_format_error}
def validate_response_format(nil), do: {:ok, nil}
def validate_response_format(%{} = map) do
case Map.get(map, "type") || Map.get(map, :type) do
nil -> {:error, {:missing_response_format_type, map}}
type when is_binary(type) and byte_size(type) > 0 -> {:ok, map}
other -> {:error, {:invalid_response_format, other}}
end
end
def validate_response_format(other), do: {:error, {:invalid_response_format, other}}
# ---------------------------------------------------------------------------
# Reasoning preamble
#
# Injected before every skill's system_prompt to establish baseline reasoning
# discipline. This is a distilled version of the coordinator's reasoning and
# evidence-hygiene guidelines, adapted for autonomous skill agents that don't
# have interactive back-and-forth with the user.
# ---------------------------------------------------------------------------
@reasoning_preamble """
## Reasoning discipline
Think step-by-step. Establish facts, then relationships, then conclusions.
Evidence hygiene:
- Cite only observable artifacts (file paths, line numbers, function names, log output).
- Connect facts explicitly: "X because Y" - not "X might be related to Y."
- Prefer the minimal sufficient chain of evidence. Short, correct, and traceable
beats long and speculative.
Validation and uncertainty:
- Identify assumptions and validate them against the source before relying on them.
- If uncertainty remains after investigation, state it plainly and explain what
would resolve it. Do not speculate past what the evidence supports.
- Tag unknowns explicitly (e.g., "Uncertain: X - could not confirm because Y").
Critical stance:
- Challenge weak premises and missing data early.
- Do not guess when the risk of being wrong is high. Say what you don't know.
"""
@doc """
Returns the reasoning preamble that is prepended to every skill's system prompt.
This establishes baseline reasoning discipline for all skill agents.
"""
@spec reasoning_preamble() :: String.t()
def reasoning_preamble, do: @reasoning_preamble
@doc """
Return the list of allowed toolboxes.
Toolboxes are the skill's tool tags; they select tool groups.
"""
@spec allowed_toolboxes() :: [tool_tag]
def allowed_toolboxes(), do: AI.Tools.skill_tool_tags()
@doc """
Return the list of allowed model presets.
This list is intended for interactive selection.
"""
@spec allowed_model_presets() :: [String.t()]
def allowed_model_presets() do
[
"smart",
"balanced",
"fast",
"web",
"large_context",
"large_context:smart",
"large_context:balanced",
"large_context:fast"
]
end
end