Packages

fnord

0.9.20

AI code archaeology

Current section

Files

Jump to
fnord lib ai tools run_skill.ex
Raw

lib/ai/tools/run_skill.ex

defmodule AI.Tools.RunSkill do
@moduledoc """
Execute an enabled Skill by name.
This is an integration point between the coordinator and Skills. It:
- lists skills dynamically in its tool spec,
- resolves a selected skill by name,
- enforces RW gating (skills requiring `rw` tools need `--edit`),
- enforces a recursion depth limit for skill-to-skill calls,
- runs the skill via `AI.Agent.Skill`.
"""
@behaviour AI.Tools
@tool_name "run_skill"
@impl AI.Tools
def async?, do: false
@impl AI.Tools
def is_available?, do: true
@impl AI.Tools
def read_args(%{} = args) do
with {:ok, skill} <- get_string(args, "skill"),
{:ok, prompt} <- get_string(args, "prompt") do
{:ok, %{"skill" => skill, "prompt" => prompt}}
end
end
defp get_string(args, key) do
case Map.fetch(args, key) do
{:ok, value} when is_binary(value) and byte_size(value) > 0 -> {:ok, value}
_ -> {:error, {:missing_arg, key}}
end
end
@impl AI.Tools
def spec() do
names = skills_names()
details = skills_descriptions()
%{
type: "function",
function: %{
name: @tool_name,
description: "Run a named skill.\n\nAvailable skills: #{names}",
parameters: %{
type: "object",
required: ["skill", "prompt"],
additionalProperties: false,
properties: %{
skill: %{
type: "string",
description: "Skill name. Available skills:\n#{details}"
},
prompt: %{
type: "string",
description: "Input prompt to send to the skill"
}
}
}
}
}
end
defp skills_names() do
case Skills.list_enabled() do
{:ok, []} -> "(none)"
{:ok, skills} -> Enum.map_join(skills, ", ", & &1.name)
{:error, _} -> "(none)"
end
end
defp skills_descriptions() do
case Skills.list_enabled() do
{:ok, []} ->
"(none found)"
{:ok, skills} ->
skills
|> Enum.map(fn %{name: name, effective: %{skill: skill}} ->
"- #{name}: #{skill.description}"
end)
|> Enum.join("\n")
{:error, reason} ->
"(error loading skills: #{inspect(reason)})"
end
end
@impl AI.Tools
def call(%{"skill" => name, "prompt" => prompt}) do
# Soft-gating: when max depth is reached, still run the skill but strip
# its ability to call other skills (mirrors the researcher pattern).
can_recurse? =
case Services.SkillDepth.inc_depth() do
{:ok, _depth} -> true
{:error, :max_depth_reached} -> false
end
try do
with {:ok, resolved} <- Skills.get_enabled(name),
:ok <- enforce_rw_gating(resolved) do
run_skill(resolved, prompt, not can_recurse?)
end
after
case can_recurse? do
true -> Services.SkillDepth.dec_depth()
false -> :ok
end
end
end
defp enforce_rw_gating(%{effective: %{skill: %Skills.Skill{tools: tools}}}) do
if Enum.member?(tools, "rw") and not Settings.get_edit_mode() do
{:error, {:denied, "Cannot run rw skills; the user did not pass --edit."}}
else
:ok
end
end
defp run_skill(%{effective: %{skill: skill}}, prompt, strip_skills?) do
agent = AI.Agent.new(AI.Agent.Skill)
AI.Agent.get_response(agent, %{
skill: skill,
prompt: prompt,
strip_skills?: strip_skills?
})
end
@impl AI.Tools
def ui_note_on_request(%{"skill" => skill}) do
{"Run skill", skill}
end
@impl AI.Tools
def ui_note_on_result(_args, _result) do
nil
end
@impl AI.Tools
def tool_call_failure_message(_args, _error) do
:default
end
end