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,
    }
}
main.py — ParseTicketOp.Run
try:
    parsed = json.loads(raw)
except json.JSONDecodeError as e:
    raise library.ErrRepairable(
        prompt=f"The text below should be valid ticket JSON, but parsing failed:\n  {e}\n\n{ticket_schema_spec}\n\nInput:\n{raw}\n\nOutput corrected JSON only — the entire object, not a patch. No code fences.",
        cause=e
    )
index.ts — ParseTicketOp.Run
try {
  data = JSON.parse(raw);
} catch (e) {
  throw new Error(`The text below should be a valid ticket JSON, but parsing failed:\n  ${e}\n\n${TICKET_SCHEMA_SPEC}\n\nInput:\n${raw}\n\nOutput corrected JSON only — the entire object, not a patch. No code fences.`);
}

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
}
main.py — UnmarshalRepair
# In Python, decoding and encoding are handled by codec functions passed to repair
def unmarshal_repair(response: str) -> dict:
    cleaned = strip_code_fences(response)
    return json.loads(cleaned)
index.ts — UnmarshalRepair
// In TypeScript, codec encodes and decodes the types to strings
codec: {
  encode: (v) => JSON.stringify(v),
  decode: (v) => JSON.parse(v)
}

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",
    },
)
main.py — init()
library.register_with_repair(
    "ParseTicketRepair",
    lambda: ParseTicketOp(),
    library.RepairConfig(
        input_field="raw",
        max_attempts=3,
        prompt_prefix="You are a strict JSON corrector. Output the corrected JSON only.\n\n",
    )
)
index.ts — init()
const parsed = wf.ai.repair(
  rawInput,
  { 
      name: "repair_parse",
      codec: { encode: (v) => v, decode: (v) => v },
      run: (raw: string) => parseTicket(raw)
  }
);

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
shell
export CLAUDE_API_KEY=<your key>

# malformed JSON
python examples/with_repair/main.py --input @examples/with_repair/testdata/dirty-format.json

# urgent ticket missing escalation_contact
python examples/with_repair/main.py --input @examples/with_repair/testdata/dirty-business-rule.json --reasoning
shell
export CLAUDE_API_KEY=<your key>

# malformed JSON
npx tsx examples/with_repair/index.ts --input @examples/with_repair/testdata/dirty-format.json

# urgent ticket missing escalation_contact
npx tsx examples/with_repair/index.ts --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
shell
# Speak MCP over stdin/stdout instead of running once
python examples/with_repair/main.py --mcp
shell
# Speak MCP over stdin/stdout instead of running once
npx tsx examples/with_repair/index.ts --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 →