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
-
.augment_prompt_with_validation_errors(prompt, errors) ⇒ Object
Append a fix-it block describing the prior attempt's errors.
- .exhausted_envelope(max_attempts, attempts) ⇒ Object
-
.format_validation_errors(errors) ⇒ Object
Render structured errors as a compact, model-readable list.
-
.run_generation_with_recovery(base_prompt:, invoke_subagent:, build_envelope:, catalog: nil, config: nil, on_attempt: nil) ⇒ Object
Drive the validate→retry loop.
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 |