Current section
Files
Jump to
Current section
Files
src/plugins/adk_plugin_reflect_retry.erl
%% @doc Convert a tool failure into bounded, model-visible retry guidance.
-module(adk_plugin_reflect_retry).
-behaviour(adk_plugin).
-export([on_tool_error/3]).
-define(DEFAULT_GUIDANCE,
<<"Reflect on the tool error, correct the arguments, and retry only "
"when the operation is safe and idempotent.">>).
on_tool_error(Context, Failure, Config) when is_map(Config) ->
Enabled = maps:get(enabled, Config, true),
MaxAttempts = maps:get(max_attempts, Config, 1),
Attempt = maps:get(<<"retry_attempt">>, Context,
maps:get(retry_attempt, Context, 0)),
Guidance = maps:get(guidance, Config, ?DEFAULT_GUIDANCE),
case valid_config(Enabled, MaxAttempts, Guidance) of
false -> erlang:error(invalid_reflect_retry_plugin_config);
true when Enabled =:= false -> observe;
true when Attempt >= MaxAttempts -> observe;
true ->
{return,
#{<<"adk_retry">> =>
#{<<"retryable">> => true,
<<"attempt">> => Attempt + 1,
<<"max_attempts">> => MaxAttempts,
<<"guidance">> => Guidance,
<<"error">> => failure_summary(Failure)}}}
end.
valid_config(Enabled, MaxAttempts, Guidance) ->
is_boolean(Enabled) andalso is_integer(MaxAttempts) andalso
MaxAttempts > 0 andalso MaxAttempts =< 16 andalso
is_binary(Guidance) andalso byte_size(Guidance) > 0 andalso
byte_size(Guidance) =< 4096.
failure_summary({adk_failure, Failure}) when is_map(Failure) ->
#{<<"component">> => safe_scalar(maps:get(component, Failure, unknown)),
<<"operation">> => safe_scalar(maps:get(operation, Failure, unknown)),
<<"class">> => safe_scalar(maps:get(class, Failure, external)),
<<"reason">> => safe_scalar(maps:get(reason, Failure,
external_failure))};
failure_summary(_Failure) ->
#{<<"reason">> => <<"external_failure">>}.
safe_scalar(Value) when is_binary(Value) -> Value;
safe_scalar(Value) when is_atom(Value) -> atom_to_binary(Value, utf8);
safe_scalar(Value) when is_integer(Value) -> Value;
safe_scalar(_) -> <<"external_failure">>.