Packages

fnord

0.9.39

AI code archaeology

Current section

Files

Jump to
fnord lib ai agent skill.ex
Raw

lib/ai/agent/skill.ex

defmodule AI.Agent.Skill do
@moduledoc """
Generic agent implementation for executing a `%Skills.Skill{}`.
The coordinator (or another agent) provides:
- the skill to run, and
- an input prompt.
This agent resolves the skill's runtime configuration:
- model preset (via `Skills.Runtime.model_from_string/1`)
- tool tags (via `Skills.Runtime.toolbox_from_tags/1`)
- response_format (via `Skills.Runtime.validate_response_format/1`)
It then performs a single completion using the resolved model, toolbox, and
messages.
"""
@behaviour AI.Agent
@type error ::
Skills.Runtime.model_error()
| Skills.Runtime.toolbox_error()
| Skills.Runtime.response_format_error()
| {:missing_arg, atom}
@impl AI.Agent
@spec get_response(map) :: {:ok, any} | {:error, any}
def get_response(opts) do
with {:ok, agent} <- fetch_arg(opts, :agent),
{:ok, skill} <- fetch_arg(opts, :skill),
{:ok, prompt} <- fetch_arg(opts, :prompt),
:ok <- enforce_rw_gating(skill),
{:ok, model} <- Skills.Runtime.model_from_string(skill.model),
{:ok, toolbox} <- Skills.Runtime.toolbox_from_tags(skill.tools),
{:ok, response_format} <- Skills.Runtime.validate_response_format(skill.response_format) do
UI.report_from(agent.name, "[skill #{skill.name}] #{prompt}")
# When at max skill nesting depth, strip the run_skill tool so the skill
# can still execute but cannot recurse further (mirrors researcher pattern).
toolbox =
case Map.get(opts, :strip_skills?, false) do
true -> Map.drop(toolbox, ["run_skill"])
false -> toolbox
end
# Prepend the shared reasoning preamble to the skill's own system prompt
# so all skill agents inherit baseline reasoning discipline.
system_prompt = Skills.Runtime.reasoning_preamble() <> "\n" <> skill.system_prompt
AI.Agent.get_completion(agent,
model: model,
# Under the Responses API, web search is a tool entry attached via
# web_search?, not a property of the model - the "web" preset resolves
# to a plain model. Without this flag, a skill declaring model = "web"
# would silently run without search.
web_search?: skill.model == "web",
toolbox: toolbox,
response_format: response_format,
log_msgs: true,
messages: [
AI.Util.system_msg(AI.Util.project_context()),
AI.Util.system_msg(system_prompt),
AI.Util.user_msg(prompt)
]
)
|> case do
{:ok, %{response: response}} -> {:ok, response}
{:error, %{response: response}} -> {:error, response}
{:error, reason} -> {:error, reason}
end
else
{:error, reason} -> {:error, reason}
end
end
# RW skills require --edit mode. Enforced here so direct callers (not just
# the RunSkill tool) cannot bypass the gate.
defp enforce_rw_gating(%Skills.Skill{tools: tools}) do
if Enum.member?(tools, "rw") and not Settings.get_edit_mode() do
{:error, "Cannot run rw skills; the user did not pass --edit."}
else
:ok
end
end
defp fetch_arg(opts, key) do
case Map.fetch(opts, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, {:missing_arg, key}}
end
end
end