Examples WithRepair — AI-assisted recovery
Repair example

WithRepair — AI-assisted recovery

A support ticket pipeline that ingests raw JSON and runs it through two repair-wrapped stages. First, a JSON parser corrects malformed input or schema violations. Second, a routing validator enforces business rules and round-trips the offending ticket through XML when it needs the LLM to fix it. Both wire-format paths of the wrapper — string repair and struct repair — exercised in one workflow.

What this example teaches

  • Returning *library.ErrRepairable from a deterministic op to opt in to LLM-driven recovery
  • Implementing UnmarshalRepair — once with simple string handling, once round-tripping through XML
  • Registering two repair-wrapped ops with library.RegisterWithRepair and different RepairConfig values
  • Mixing format-level repair (malformed JSON) and business-rule repair (urgent ticket without escalation contact) in the same workflow
  • Inspecting the per-attempt repair trail via library.WithReasoningLog
View source on GitHub
The workflow

Three vertices, two repair wrappers

ContextValOp
raw ticket text
ParseTicketRepair
WithRepair · JSON corrector
ValidateRoutingRepair
WithRepair · XML corrector
validated
TicketInput

From the graph builder's perspective the two repair-wrapped vertices look exactly like the inner ops — same input / output field names. The wrapper only activates when the inner Run returns *library.ErrRepairable.

Implementation

Walkthrough

1. Return *ErrRepairable from the inner op

ParseTicketOp attempts JSON deserialisation and then runs schema checks (id format, email shape, valid priority, non-empty summary). Both failure paths return *library.ErrRepairable carrying a self-contained prompt that includes the bad input and the schema spec — everything the LLM needs to produce a corrected JSON object without further context.

main.go — ParseTicketOp.Run
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
    return &library.ErrRepairable{
        Prompt: fmt.Sprintf(
            "The text below should be valid ticket JSON, but parsing failed:\n  %s\n\n%s\n\nInput:\n%s\n\nOutput corrected JSON only — the entire object, not a patch. No code fences.",
            err, ticketSchemaSpec, raw,
        ),
        Cause: err,
    }
}

2. Implement UnmarshalRepair on the input type

The wrapper hands the LLM's response to UnmarshalRepair. For the parse stage the input is a thin TicketRaw wrapper around a string — just strip code fences and assign. For the validate stage the input is *TicketInput and the wrapper expects XML, so UnmarshalRepair calls xml.Unmarshal.

main.go — UnmarshalRepair
func (t *TicketRaw) UnmarshalRepair(response string) error {
    t.Text = stripCodeFences(response)
    return nil
}

func (t *TicketInput) UnmarshalRepair(response string) error {
    cleaned := stripCodeFences(response)
    if err := xml.Unmarshal([]byte(cleaned), t); err != nil {
        return fmt.Errorf("xml.Unmarshal: %w", err)
    }
    return nil
}

3. Register each repair wrapper with its config

One init() call per wrapped op. Pick the input field to repair, set an attempt budget, and supply a prompt prefix that primes the model for the response shape it should emit. Different stages can have different budgets (3 attempts to fix a JSON parse, 2 to fix a routing violation).

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

library.RegisterWithRepair(
    "ValidateRoutingRepair",
    func() *ValidateRoutingOp { return &ValidateRoutingOp{} },
    library.RepairConfig{
        InputField:   "Ticket",
        MaxAttempts:  2,
        PromptPrefix: "You are a strict XML ticket corrector. Output corrected XML only.\n\n",
    },
)

Run it

Sample inputs ship under testdata/. Pass --reasoning to enable WithReasoningLog and see exactly which attempt corrected the ticket.

shell
export CLAUDE_API_KEY=<your key>

# malformed JSON
go run ./examples/with-repair --input @examples/with-repair/testdata/dirty-format.json

# urgent ticket missing escalation_contact
go run ./examples/with-repair --input @examples/with-repair/testdata/dirty-business-rule.json --reasoning

Run it as an MCP server

The same binary is dual-mode. Pass --mcp and instead of running once and exiting it speaks the Model Context Protocol over stdin/stdout, exposing the entire workflow as a single MCP tool, parse_and_validate_ticket. The Go SDK derives the tool's input schema from the workflow's UserInput struct and validates every tools/call request against it; the AI repair attempts happen server-side, so the caller only ever sees the validated ticket or a clean error.

shell
# Speak MCP over stdin/stdout instead of running once
go run ./examples/with-repair --mcp

Register it with any MCP client by pointing the client at that command:

.mcp.json
{
  "mcpServers": {
    "with-repair": {
      "command": "go",
      "args": ["run", "./examples/with-repair", "--mcp"]
    }
  }
}
← RAG (Gemini embeddings) All examples →