agentsope/agentsop-output-format-by-model
>-
npx skills add https://github.com/agentsope/SkillAlchemy --skill agentsop-output-format-by-model
> One-liner: Different output formats carry different cognitive load for the model. Code-in-JSON is the canonical proof: the same model writes worse code when wrapped in a JSON tool-call than when emitted as plain text + diff. The reverse failure (asking for prose when you need a typed object) is just as common. Pick format per (task × consumer), not by reflex.
Activate this skill before committing to an output schema in any of these situations:
| Trigger | Signal |
|---|---|
| Designing a coder-agent | "should the model return a apply_patch tool call or plain-text diff?" |
| Adding a tool to an existing agent | "tool input has a code / query / sql / regex field — should I nest it in JSON or leave it as a string?" |
| Building extraction / classification | "should I use dspy.Predict typed fields, Pydantic + response_format=json_schema, or just markdown?" |
| Wiring an evaluator | "the metric needs a number — but the model also has to *reason* to produce it" |
| Migrating a working prompt to "structured outputs" | someone said "let's make it safer with JSON schema" |
| Tool-call harness adds latency / errors | repeated json.JSONDecodeError, escaping bugs, truncated outputs |
Anti-triggers (skip this skill):
┌──────────────────────────────────────────────────┐
│ FORMAT FOLLOWS FUNCTION │
│ │
│ Some formats add cognitive load to the model │
│ and measurably degrade quality on the │
│ *content* the format is supposed to wrap. │
└──────────────────────────────────────────────────┘
▲ ▲
│ │
What's being consumed? Who consumes it?
(code? prose? entities? (human reader? parser?
number? action selection?) downstream LM? compiler?)
│ │
└──────────┬───────────────┘
▼
FORMAT SELECTION
(text+diff | markdown | JSON | tool_use
| grammar-constrained | typed field)
Aider's code-in-json benchmark is the load-bearing empirical anchor:
SyntaxError / IndentationError. Sonnet kept syntax clean but still scored lower overall. [aider.chat/2024/08/14/code-in-json.html]Academic generalization, same year:
| Reflex | When it's wrong |
|---|---|
| "Structured output is always safer." | False for code, multi-step reasoning, free-form prose. Strictness ≠ quality of contents. |
| "Markdown is only for humans." | False — markdown is *also* the highest-fidelity wire format for many LM-to-LM hand-offs (Aider uses it; DSPy's default chat adaptor uses field-marked markdown over JSON for many signatures). |
Every formatting requirement consumes some of the model's attention budget. The tax is:
\n becomes \\n; each quote becomes \"; the model has to track this *while also* solving the actual problem.⇒ Heuristic: the more semantically dense the content, the cheaper the format must be.
Three questions, in order:
| Content type | Default format | Why |
|---|---|---|
| Source code edits | plain text + diff format (SEARCH/REPLACE, unified diff, or str_replace tool with code as a *single string field*) | Aider 20%→61% on GPT-4 Turbo; same direction across all models. [aider.chat/2024/08/14/code-in-json.html] |
| Source code (full file rewrite) | plain text or markdown fenced block | Same reason. JSON-wrap adds escaping tax. |
| Structured data extraction (entities, dates, IDs, classifications) | JSON / Pydantic / typed OutputField | Schema *helps* here — fields are the task. |
| Action selection (which tool to call) | JSON tool_use | Tool name + scalar args. The decision is structured by definition. |
| Action *body* (SQL, regex, code, file contents) | single string field inside tool_use — do not sub-structure | Same code-in-JSON penalty applies to any code-shaped payload. |
| Reasoning / intermediate steps | markdown or Python-style scratchpad | CoT in JSON measurably degrades; see "Let Me Speak Freely?" [arxiv.org/abs/2408.02442] |
| Numeric answer with reasoning | markdown reasoning + final answer in fenced block or final-line convention | Don't force the reasoning into a JSON reasoning field — it shortens and stiffens. |
| Free-form prose (summary, explanation, customer reply) | markdown | Native to instruction-tuned models. |
| Mixed (e.g., extract entities AND rewrite the document) | split into two calls or two passes — see §5 Case B | One format can't serve two contents well. |
| Consumer | Constraint | Implication |
|---|---|---|
| Human in a chat UI | Render-friendly | Markdown wins. JSON is hostile. |
| json.loads / Pydantic parser | Must be valid | JSON with schema, or a known-safe envelope (<result>...</result>) with markdown body. |
| Downstream LM (LM-to-LM pipeline) | Reads what was written | Markdown is *more* robust than JSON when content includes code/math; the next LM ingests it natively. |
| Compiler / interpreter (PoT, code-exec sandbox) | Must be a valid program | Output as code in a fenced block, not as a JSON program field. |
| Tool dispatcher (function calling) | Needs tool name + args | JSON tool_use with scalar args; multi-line bodies go in a single string field. |
| Diff applier (Aider, git apply, str_replace) | Must apply cleanly | Format dictated by the applier: SEARCH/REPLACE for Aider diff, unified diff for git, str_replace for Anthropic editor tool. |
Format support is model-specific. Aider maintains a per-model edit-format default precisely because of this. [aider.chat/docs/more/edit-formats.html]
| Model family | Best code-edit format | Notes |
|---|---|---|
| GPT-4 Turbo / GPT-4o | udiff or SEARCH/REPLACE diff | 20%→61% with udiff on refactor. [aider.chat/2023/12/21/unified-diffs.html] |
| Claude 3.5/3.7 Sonnet | SEARCH/REPLACE diff | Tendency to write *too much* — instruct minimal blocks. [aider.chat/2024/07/01/sonnet-not-lazy.html] |
| Gemini family | diff-fenced | Path inside the fence. [aider.chat/docs/more/edit-formats.html] |
| GPT-4.1 / OpenAI patch tool | patch protocol | OpenAI-specific, multi-action robust. |
| Weak / local models (Llama-3-8B class, GPT-3.5) | whole file rewrite | Diff parsing failures dominate; whole-file is dumb-but-stable. |
| Reasoning models (o1, o3) as architect | architect mode: reasoner emits prose plan → editor model emits diff | o1-preview alone: 79.7%; o1-preview + Sonnet editor: 82.7%. [aider.chat/2024/09/26/architect.html] |
Combined decision tree:
What's being emitted?
│
┌───────────────────┼───────────────────┐
│ │ │
code structured free-form
edits data fields prose/reasoning
│ │ │
▼ ▼ ▼
diff/SEARCH- JSON / Pydantic / markdown
REPLACE/ tool_use scalar
str_replace, args
code as string
│ │ │
▼ ▼ ▼
pick per-model wrap any code- do NOT
edit format shaped payload in force into
(Aider table) a single string field JSON schema
Ten concrete operations. Format choice is *not* arbitrary — each citation is the empirical anchor.
| # | Task | Recommended format | Rationale / Evidence |
|---|---|---|---|
| 1 | "Edit auth.py to use JWT" (coder-agent core loop) | Plain-text SEARCH/REPLACE diff in markdown fence | Aider's measured 3× on GPT-4 Turbo, generalizes across models. [aider.chat/2024/08/14/code-in-json.html] |
| 2 | "Apply this patch to a file via Claude's text-editor tool" | Anthropic str_replace tool — old_str / new_str as string fields, no JSON sub-structure inside the code | Matches Anthropic's published tool-design guidance: fields should be high-signal scalars, not low-level identifiers. [docs.anthropic.com text-editor-tool] |
| 3 | "Extract invoice fields (vendor, amount, date, line items)" | JSON / Pydantic / DSPy typed OutputField | Schema is the task. Structured-output benchmarks show high accuracy here. [arxiv.org/html/2505.20139v1] |
| 4 | "Classify ticket priority (P0/P1/P2/P3)" | Single-token output or JSON {"priority": "..."} | Trivial; structure helps determinism. |
| 5 | "Answer a math word problem" | Markdown reasoning + final answer in \boxed{} or fenced final line | "Let Me Speak Freely?" — 10–15% degradation when locked into JSON reasoning. [arxiv.org/abs/2408.02442] |
| 6 | "Decide which tool to call next" | JSON tool_use with tool name + scalar args | Action selection is *intrinsically* structured. |
| 7 | "Generate the SQL query for that tool" | Tool args contain {"sql": "SELECT ..."} — SQL as a raw single string, no further JSON sub-structure | Same family as code-in-JSON; SQL is code-shaped. |
| 8 | "Write a customer-support reply" | Markdown | Native to instruction tuning; JSON-wrap costs nothing useful. |
| 9 | "Summarize a paper and return key claims as a list" | Markdown prose + a fenced claims: YAML or JSON block at the end (two-zone output) | Best of both: prose flows freely, downstream parser reads the trailing block. |
| 10 | "Rewrite a long document AND extract entities" | Two passes: pass 1 rewrite (markdown), pass 2 extract (JSON, fed the pass-1 output) | One call cannot serve both contents at peak quality. |
Trigger: New extraction pipeline. Fields = vendor_name, total_amount, invoice_date, line_items[].
Constraints:
Decision steps:
response_format=json_schema or DSPy typed OutputFields. Field names carry the semantic load (DSPy's "Signatures carry semantic load" principle [dspy.ai/learn/programming/signatures]).line_items[].description containing free-text: still inside JSON — descriptions are *prose data*, not code. Escaping cost is real but bounded.Outcome: JSON wins. Schema validation catches missing fields cheaply; no measurable degradation expected on this content shape.
Trigger: PM adds requirement — "and produce a cleaned-up version of the invoice document body."
Constraints:
cleaned_text: "..." inside the same JSON forces multi-paragraph escaping and competes with the extraction reasoning for attention.Decision steps:
{vendor, amount, date, line_items} as JSON. Call 2 receives the original doc + extracted JSON, returns markdown rewrite.===EXTRACTION=== separator, then a fenced JSON block. Parser splits on the separator. Cheaper but more fragile.Outcome: Plain text + JSON split, not unified JSON. Format follows content, even within one logical task.
Trigger: Building a data-analyst agent. One tool is run_query(sql: str).
Constraints:
Decision steps:
run_query vs read_file vs list_tables.sql a single top-level string field. Don't sub-structure it ({from: ..., where: ...}). Let the model write idiomatic SQL.dry_run: bool).`sql block), then a thin downstream agent wraps it into the tool call. This is the Aider "architect" pattern transplanted to data tools. [aider.chat/2024/09/26/architect.html]Outcome: JSON for the decision; single string for the SQL body; consider a two-step generate-then-dispatch if quality matters.
Trigger: Eng-org-wide push to add response_format=json_schema to every LM call.
Constraints:
json.loads failures.Decision steps:
json_schema. This is where structured output earns its keep.Outcome: Reject the blanket policy; replace it with a content-type-driven one. This is the inverse of the "JSON everywhere" reflex.
{language, lines: [...], imports: [...]}. Every level of nesting compounds the escape tax.reasoning JSON field. Models compress and stiffen when reasoning is inside JSON — they treat it as a label rather than as thinking. Prefer free markdown reasoning followed by a structured tail.name, file_type) over technical identifiers (uuid, mime_type). The model reads your schema as part of its prompt. [anthropic.com/engineering/writing-tools-for-agents]10. "The task is fixing JSON, so format doesn't matter." Counter-edge case: when the *content itself is JSON* (e.g. "fix this malformed JSON"), output it as a fenced `json block in markdown — not as a JSON tool-call wrapping JSON-as-string. Even here, the rule holds: code-shaped content goes in a plain string surface, not nested escapes. [arxiv.org/html/2510.04717v1 — JSON Whisperer]
How major frameworks implement format selection. Use this to translate the principles into the stack you're already in.
| Format | Wire shape | When |
|---|---|---|
| whole | Full file in markdown fence | Weak models, fallback |
| diff (SEARCH/REPLACE) | Two fences per edit, byte-exact match | GPT-4o, Sonnet, most strong models — default |
| diff-fenced | Path inside fence | Gemini |
| udiff | GNU unified-diff style | GPT-4 Turbo — the empirical anchor: 20%→61% |
| patch | OpenAI patch protocol | GPT-4.1 |
| editor-diff / editor-whole | Slim prompt for sub-model | Architect mode editor |
No JSON-wrapped edit format exists — it was tested and rejected. [aider.chat/2024/08/14/code-in-json.html]
DSPy's Signature → Module pipeline ships with multiple adaptors that materialize the same logical I/O contract into different wire formats:
ChatAdapter (default): field-marked markdown with [[ ## field_name ## ]] headers. Used even for typed outputs because models reliably emit it.JSONAdapter: schema-enforced JSON. Used when the consumer must parse strictly (e.g., feeding another typed pipeline).TwoStepAdapter: free-form generation, then a second cheaper call reformats into JSON. Direct application of the "Let Me Speak Freely?" finding — generate freely, format separately. [arxiv.org/abs/2408.02442]DSPy's recommendation: stay on ChatAdapter unless a downstream consumer demands JSON. The framework itself encodes the principle of this skill.
response_format=json_schema for typed extraction: yes. For code generation: avoid; if forced, single string field with code as the type.strict: true) guarantees schema validity but does not fix content degradation — Aider tested this explicitly. [aider.chat/2024/08/14/code-in-json.html]text_editor tool: commands view, str_replace, create, insert, undo_edit. str_replace takes old_str and new_str as single string fields — explicitly avoiding the JSON-nests-code anti-pattern. This is the principle of this skill made into a first-party API.Layer | Format mechanism
-----------------------|-----------------------------------
Generation grammar | Outlines / Guidance — enforce
Per-task adaptor | DSPy ChatAdapter vs JSONAdapter
Per-model edit format | Aider's edit-format table
Tool envelope | OpenAI / Anthropic tool_use
Wire serialization | markdown vs JSON vs YAML
Every layer can independently make a wrong format choice. This skill operates at the per-task layer: decide first *what format the content wants*, then let each lower layer enforce it.
┌──────────────────────────────────────────────────────────────────────┐
│ FORMAT DECISION CARD │
├──────────────────────────────────────────────────────────────────────┤
│ Code edits → text + diff (per-model: udiff/SEARCH/whole) │
│ [Aider: 20% → 61% on GPT-4 Turbo] │
│ Code generation → markdown fenced block │
│ SQL / regex / shell → string field inside tool_use, NOT sub-JSON │
│ Entity extraction → JSON / Pydantic / typed OutputField │
│ Classification → single token or JSON {label} │
│ Action selection → JSON tool_use │
│ Reasoning + answer → markdown CoT + fenced final answer │
│ [Format-restricted reasoning: -10 to -15%] │
│ Prose / explanation → markdown │
│ Mixed content → two passes OR two-zone output │
├──────────────────────────────────────────────────────────────────────┤
│ NEVER: │
│ • Nest code inside JSON sub-structure │
│ • Force CoT reasoning into a JSON "reasoning" field │
│ • Use schema as substitute for prompt engineering │
│ • Assume strict-mode JSON fixes content quality │
└──────────────────────────────────────────────────────────────────────┘
Primary empirical anchors:
Academic generalization:
Framework / API docs:
Companion skills in this collection:
aider-sop-skill/SKILL.md — full edit-format treatment in coder-agent context.dspy-sop-skill/SKILL.md — adaptor selection within compiled programs.Take agentsope/agentsop-output-format-by-model from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.