Agent behavior

Structured Output

Set response_model to a pydantic model and the agent's final text is validated against it, landing on AgentResult.parsed. One automatic repair attempt is made if the first response isn't valid JSON matching the schema; if that also fails, OutputParseError is raised with the raw text attached.

structured_output_example.py
from pydantic import BaseModel
from deepcrew import Agent, run_agent

class Verdict(BaseModel):
    approved: bool
    reason: str

agent = Agent(name="reviewer", model="openai/gpt-4o", response_model=Verdict)
result = await run_agent(agent, [{"role": "user", "content": "Review this PR diff: ..."}])

result.parsed.approved  # bool
result.parsed.reason    # str

How it actually works

This is prompt-based, not provider-JSON-mode-enforced — worth knowing since it's a weaker guarantee than passing response_format={"type": "json_object"} directly to the provider. When response_model is set, deepcrew appends one extra system message to the conversation before the first turn:

text
Respond ONLY with JSON matching this schema: {"properties": {...}, "required": [...], ...}

...where the schema is response_model.model_json_schema(), pydantic's own schema dump. The model is trusted to follow that instruction; nothing on the LiteLLM call itself forces JSON output. Parsing happens once the agent's final turn produces text with no more tool calls:

  1. Any leading/trailing ```json ... ``` (or plain ```) fence around the response is stripped first.
  2. The result is validated with response_model.model_validate_json(...).
  3. On failure (invalid JSON, or valid JSON that doesn't match the schema), deepcrew makes exactly one repair attempt: it re-prompts the same model with the full conversation history plus the failed response and the validation error text, asking for corrected JSON.
  4. If the repair attempt also fails to validate, OutputParseError is raised — not returned as a degraded result — carrying the repaired (still-invalid) text on .raw_text.
python
from deepcrew import OutputParseError

try:
    result = await run_agent(agent, [{"role": "user", "content": "..."}])
except OutputParseError as exc:
    print("Model never produced valid JSON, even after one repair attempt:")
    print(exc.raw_text)  # the raw (still invalid) text from the repair attempt

Combining with tools and with LoopConfig

An agent with both response_model and tools/skills works fine: the schema instruction is injected once as a system message, and the agent can still make tool calls across multiple turns as usual — parsing only happens against the text of the final turn (the one with no more tool calls), never against intermediate turns.

response_model also composes with loop_config: every outer-loop iteration runs the schema-instruction + parse-with-repair logic independently, and whichever AgentResult the loop ultimately returns (converged, adaptive-stopped, or the last iteration) carries that iteration's own .parsed value. If you want convergence to depend on the parsed structure rather than just the raw text, write a convergence_fn that inspects result.parsed directly — nothing does that for you automatically.

Common pitfalls

  • This is a prompt instruction, not enforced JSON mode. Weaker models or highly complex schemas can still produce non-JSON text on both the first attempt and the repair attempt, ending in OutputParseError.
  • Only one repair attempt, always. There's no configurable retry count for structured-output parsing specifically (separate from RetryPolicy, which governs LLM-call-level failures, not schema-validation failures).
  • result.parsed is None whenever response_model isn't set. Always None-check it, or make it a habit to only read .parsed on agents you know were constructed with a schema.
  • The repair prompt reuses the same model. There's no way to route the repair attempt to a different (e.g. stronger) model than the one that produced the invalid output.

See also

  • Looping — how response_model behaves across outer-loop iterations.
  • Human-in-the-Loop Hooks — the other "Agent Behavior" feature, for intercepting tool calls rather than validating final output.
Want an AI coding agent to wire this up for you? Click Copy prompt above and paste it into Claude, ChatGPT, or your IDE's AI assistant.