Synoema

Error Feedback Contract

The JSON shape every LLM tool sees when Synoema rejects code

Generation pipelines that feed compiler output back into the model don't want a stack trace — they want the smallest set of fields that lets the next attempt succeed. sno --errors json emits exactly that. Three fields are added on top of the standard error span: llm_hint (an actionable instruction), fixability (a difficulty label), and did_you_mean (a syntactic alternative).

Output fields

FieldTypeDescription
llm_hintstring?Actionable fix instruction phrased for an LLM — "Change the expression to produce X", not "expected X found Y"
fixability"trivial" | "easy" | "medium" | "hard"Coarse-grained difficulty signal so the orchestrator can choose retry budget
did_you_meanstring?Suggested alternative syntax for the most common mistakes (Python/Haskell habits)

Enriched error codes

CodeFixabilityLLM hint summary
type_mismatchtrivialChange expression to produce expected type
arity_mismatchtrivialAdd or remove arguments
unbound_variableeasyCheck spelling, add parameter or definition
infinite_typehardBreak cycle with an ADT wrapper
pattern_mismatcheasyCheck constructor names and arity
unexpected_tokentrivialCheck syntax and follow did_you_mean
unterminated_stringtrivialAdd closing quote
no_matcheasyAdd catch-all pattern
division_by_zerotrivialGuard with conditional
linear_unusedeasyUse the variable or remove the binding
linear_duplicateeasyUse the variable exactly once
indentationeasyFollow offside rule, 2-space indent

Did-you-mean rules

LLM writesSynoema suggests
if x then y else z? x -> y : z
[1, 2, 3][1 2 3] — no commas
return xjust x — expression-based
x -> y (lambda)\x -> y — needs backslash

These map the four most-common Python/Haskell habits LLMs fall into. The did_you_mean field carries the corrected snippet, ready to splice into the next attempt.

JSON example

{
  "code": "type_mismatch",
  "severity": "error",
  "message": "expected Int, found String",
  "span": {"line": 3, "col": 14, "end_line": 3, "end_col": 20},
  "notes": ["expected: Int", "found: String"],
  "llm_hint": "Change the expression to produce Int instead of String. Common fixes: type conversion, different operator, or fix the literal value.",
  "fixability": "trivial"
}

The pre-existing fields (code, severity, message, span, notes) match the LSP error schema, so any tool that already handles LSP diagnostics absorbs Synoema's output for free.

The two output formats

FlagShapeUse when
--errors jsonLSP-compatible: { range, message, hint, related[] }Default for all new tooling. Feeds clean into LSP clients and ReAct.
--errors json-legacyFlat: { span, message, llm_hint, notes[] }Backwards compatibility with pre-Phase 27 tools. Will be removed in 0.2.x.

Feedback loop pipeline

tools/llm/feedback_loop.py is the reference orchestrator. It generates a candidate, runs sno --errors json check, enriches the JSON with the model's previous prompt, and retries with temperature decay.

# OpenAI provider, three retries, verbose
python tools/llm/feedback_loop.py \
       --prompt "Write factorial" \
       --provider openai \
       --retries 3 -v

# File-driven prompt with Anthropic provider
python tools/llm/feedback_loop.py \
       --prompt-file task.txt \
       --provider anthropic

Temperature schedule across retries: 1.0 → 0.5 → 0.2. The early high-temperature pass explores; subsequent passes converge on the narrow corner the compiler is asking for.

The bigger picture

The error-feedback contract is one of three legs that make ReAct auto-fix work:

  1. GBNF constrained decoding — the model can't even sample non-Synoema tokens. Most syntactic mistakes never make it into the output.
  2. This contract — everything that does compile but typechecks wrong gets back a fix instruction the model can act on.
  3. The ReAct loop (sno fix file.sno) — binds the two together: Thought → Action (edit) → Observation (compiler output) until the file passes.

See /llm for the full ReAct architecture and audit-log details.

Cross-references