Module: AgUi::A2ui::Recovery

Defined in:
lib/ag_ui/a2ui/recovery.rb

Overview

A2UI error-recovery loop — Ruby port of the toolkit's recovery.py.

The toolkit cannot bind/invoke a model, so the adapter supplies invoke_subagent (its model call, receives (prompt, attempt) and returns the render_a2ui args hash or nil) and build_envelope (its prepared operations envelope). This module owns the validate→retry loop using the SAME validate_components the middleware uses, so the retry decision and the paint decision agree.

Constant Summary collapse

MAX_A2UI_ATTEMPTS =

Default attempt cap (initial try + retries). Configurable per call.

3
A2UI_RECOVERY_ACTIVITY_TYPE =

Activity type the middleware/client use for the recovery status channel.

"a2ui_recovery"
NO_TOOL_CALL_ERROR =
{
  "code" => "empty_components",
  "path" => "components",
  "message" => "Sub-agent did not call render_a2ui",
}.freeze

Class Method Summary collapse

Class Method Details

.augment_prompt_with_validation_errors(prompt, errors) ⇒ Object

Append a fix-it block describing the prior attempt's errors.



37
38
39
40
41
42
43
44
# File 'lib/ag_ui/a2ui/recovery.rb', line 37

def augment_prompt_with_validation_errors(prompt, errors)
  if errors.empty?
    prompt
  else
    "#{prompt}\n\n## Previous attempt was invalid — fix these and regenerate:\n" \
      "#{format_validation_errors(errors)}\n"
  end
end

.exhausted_envelope(max_attempts, attempts) ⇒ Object



91
92
93
94
95
96
97
98
99
# File 'lib/ag_ui/a2ui/recovery.rb', line 91

def exhausted_envelope(max_attempts, attempts)
  JSON.generate(
    {
      "error" => "Failed to generate valid A2UI after #{max_attempts} attempt(s)",
      "code" => "a2ui_recovery_exhausted",
      "attempts" => attempts,
    },
  )
end

.format_validation_errors(errors) ⇒ Object

Render structured errors as a compact, model-readable list.



32
33
34
# File 'lib/ag_ui/a2ui/recovery.rb', line 32

def format_validation_errors(errors)
  errors.map { |e| "- [#{e["code"]}] #{e["path"]}: #{e["message"]}" }.join("\n")
end

.run_generation_with_recovery(base_prompt:, invoke_subagent:, build_envelope:, catalog: nil, config: nil, on_attempt: nil) ⇒ Object

Drive the validate→retry loop. Returns "attempts", "ok": the validated envelope on success, or a structured a2ui_recovery_exhausted envelope once the cap is hit. Never retries an attempt whose components validated.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ag_ui/a2ui/recovery.rb', line 50

def run_generation_with_recovery(base_prompt:, invoke_subagent:, build_envelope:,
                                 catalog: nil, config: nil, on_attempt: nil)
  max_attempts = (config || {})["maxAttempts"] || MAX_A2UI_ATTEMPTS
  attempts = []
  last_errors = []

  (1..max_attempts).each do |attempt|
    prompt = augment_prompt_with_validation_errors(base_prompt, last_errors)
    args = invoke_subagent.call(prompt, attempt)

    record =
      if args.nil? || args.empty?
        { "attempt" => attempt, "ok" => false, "errors" => [NO_TOOL_CALL_ERROR] }
      else
        components = args["components"].is_a?(Array) ? args["components"] : []
        data = args["data"].is_a?(Hash) ? args["data"] : {}
        result = Validate.validate_components(components: components, data: data, catalog: catalog)
        { "attempt" => attempt, "ok" => result["valid"], "errors" => result["errors"] }
      end

    attempts << record
    on_attempt&.call(record)

    if record["ok"]
      break { "envelope" => build_envelope.call(args), "attempts" => attempts, "ok" => true }
    end

    last_errors = record["errors"]
  end.then do |result|
    if result.is_a?(Hash)
      result
    else
      {
        "envelope" => exhausted_envelope(max_attempts, attempts),
        "attempts" => attempts,
        "ok" => false,
      }
    end
  end
end