Concepts AI-driven repair
Concept

AI-driven repair

Sparsi workflows provide built-in mechanisms for self-healing. Some deterministic ops have a fixable failure mode — a JSON blob that's almost right, or a schema violation a language model could plausibly correct. Rather than hand-coding a retry path, Sparsi provides a repair wrapper (library.WithRepair) that uses a bounded LLM pass to recover deterministic logic.

Mechanism

One bounded loop, one typed error

Per Run, the wrapper executes up to max_attempts repair cycles (default 3). Non-repairable errors propagate unchanged — the LLM is only invoked when your inner op deliberately escalates.

  1. Inner op's Run is called normally.
  2. If it returns nil, the wrapper returns nil. Done.
  3. If it returns *library.ErrRepairable, the wrapper forwards Prompt (sandwiched by configured PromptPrefix / PromptSuffix) to the LLM.
  4. The wrapper allocates a fresh value of the named input field's element type and calls UnmarshalRepair on the LLM's response.
  5. If unmarshal fails, the wrapper appends the parse error to the prompt and counts an attempt.
  6. If unmarshal succeeds, the wrapper writes the value back via SetInputField and re-runs the inner op.
  7. If the budget is exhausted, the original error is wrapped and returned.
Repair wrapper state machine Run the inner op. On nil, return nil. On a non-repairable error, propagate. On *ErrRepairable, check the attempts budget, call the LLM, unmarshal its response, and either re-run the inner op with the fixed input or loop back to the LLM with the parse error appended. Run inner op's Run(ctx) What did Run return? Return nil — done Propagate error Attempts remaining? Return wrapped error Send prompt to LLM UnmarshalRepair (response) Did the response parse cleanly? Append parse error to prompt; ++attempts SetInputField(value) ++attempts; retry op nil other error *ErrRepairable no yes fail success retry LLM with augmented prompt re-run inner op with fixed input
Solid grey arrows = single-pass control flow. Dashed purple arrows = the bounded repair loop, each traversal counting against max_attempts.

Repair retries re-execute Run, so your inner op must be idempotent or pure. Repair traces are captured by library.WithReasoningLog when enabled.

Inner-op contract

Return *ErrRepairable; implement UnmarshalRepair

Two interfaces, both on pointer receivers. The inner op decides what's repairable; the input type decides how to deserialise the LLM's response.

a repairable op
type TicketRaw struct{ Text string }

// RepairableInput — invoked with the LLM's raw response. Convert it into
// the op's input value. JSON/XML/regex/strconv — your call.
func (t *TicketRaw) UnmarshalRepair(response string) error {
    t.Text = stripCodeFences(response)
    return nil
}

type ParseTicketOp struct {
    Raw    *TicketRaw  `dag:"input"`
    Result TicketInput `dag:"output"`
}

func (op *ParseTicketOp) Run(_ context.Context) error {
    var parsed TicketInput
    if err := json.Unmarshal([]byte(op.Raw.Text), &parsed); err != nil {
        return &library.ErrRepairable{
            Prompt: "Correct this JSON to match the schema below:\n…",
            Cause:  err,
        }
    }
    op.Result = parsed
    return nil
}
Registration

Wrap once at init(); wire identically

From the graph builder's perspective, a repair-wrapped vertex looks exactly like the inner op — same input / output field names. The repair config is set at registration time and can be overridden per-vertex via params.

main.go — init()
library.RegisterWithRepair(
    "ParseTicketRepair",
    func() *ParseTicketOp { return &ParseTicketOp{} },
    library.RepairConfig{
        InputField:   "Raw",
        MaxAttempts:  3,
        PromptPrefix: "You are a strict JSON corrector. Output corrected JSON only.\n\n",
    },
)

Vertex params: max_attempts, provider (default claude), model (default claude-sonnet-4-6), max_tokens (default 2048), and the same credential routing params (credential_ref, client_factory_id) as ordinary AI ops.

When to reach for it

The boundary, again

WithRepair is not a replacement for input validation — it's a recovery path for the cases you've decided are worth a small LLM round-trip to fix.

Good fits

Malformed JSON or XML from upstream LLM output or third-party APIs that's "almost right".

Schema-violating payloads where the fix is mechanical (missing required field, wrong enum value, value out of range).

Business-rule violations a model can correct with context — e.g. urgent ticket without an escalation contact.

Lightweight summary truncation — "rewrite this to be under 280 characters while keeping the technical detail".

Bad fits

Inputs that are simply wrong — repair turns "bad request" into "expensive bad request".

Non-idempotent inner ops — every repair attempt re-runs Run. If your op writes to a database, it writes again.

Free-form generation — use AIComputeStringToStringOp directly; repair is for typed-input recovery.

Cost-sensitive hot paths — every repair attempt is an LLM call. Set max_attempts conservatively.

See it in practice