Skip to content

Structured Output and Loop State Management

When an agentic loop passes output from one stage to the next, and that output is free text, parsing is required. But free-text parsing is brittle. If the model expresses the same information in a slightly different format, parsing fails — and that failure can either halt the loop or drive it in the wrong direction.

Free-Text Parsing Failure Scenario
───────────────────────────────────────────────────────────
Expected output:
"Result: success, file: /tmp/output.txt, lines: 42"
Actual output (model may phrase it differently):
"I have successfully written 42 lines to /tmp/output.txt."
"Task complete. output.txt (42 lines)"
"Done → /tmp/output.txt | lines=42"
If the parser only handles the first format → failure
───────────────────────────────────────────────────────────

Structured output solves this. Constraining the model to return a specific JSON structure via JSON Schema nearly eliminates parsing failures.

Structured output is defined the same way as the input_schema in tool definitions. The model is instructed to return JSON conforming to a given schema and is constrained not to deviate from it.

This is illustrative pseudocode; actual API signatures differ.

# Define the output schema
task_result_schema = {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["success", "failure", "partial"],
"description": "Completion status of the task"
},
"output_path": {
"type": "string",
"description": "Absolute path of the generated file (null if none)"
},
"line_count": {
"type": "integer",
"description": "Number of lines in the output file"
},
"error_message": {
"type": "string",
"description": "Error message on failure (null on success)"
}
},
"required": ["status"]
}
# Apply the schema to the model call
response = model.generate(
messages=messages,
tools=tools,
output_schema=task_result_schema, # enforce structured output
)
# response.structured_output always conforms to the schema
result = response.structured_output
if result["status"] == "success":
process_file(result["output_path"])

An agentic loop must maintain state across iterations. That state falls into three layers:

Agent State Layers
───────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────┐
│ AGENT STATE │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Message History │ │
│ │ [user, assistant, tool, assistant, tool, ...] │ │
│ │ ↑ short-term memory inside the context window │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Metadata │ │
│ │ - iteration_count: int │ │
│ │ - start_time: datetime │ │
│ │ - total_tokens: int │ │
│ │ - goal: str (original goal — read-only) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Completion Flags │ │
│ │ - is_complete: bool │ │
│ │ - final_result: str | None │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

The most important invariant is: the goal field is never modified. As the context grows over many iterations, the original objective can be diluted or distorted — a failure mode called goal drift. Preserving the original goal in a dedicated field and re-injecting it into the system prompt at each iteration significantly reduces this risk.

Several patterns exist for carrying state between iterations, each with different trade-offs:

Pattern Mechanism Advantage Disadvantage
Full history All messages in context Simple to implement Context grows linearly
Summary compression Older messages replaced by summary Saves context budget Information loss in summary
Structured notes Key facts saved to a JSON file Selective, precise Requires upfront schema design
Hybrid Recent N messages + summary + structured notes Balanced More complex to implement

Production systems most commonly use the hybrid approach: only the most recent messages stay in the context window; long-term state is stored in structured files outside the window.

Structured Output in Agent-to-Agent Communication

Section titled “Structured Output in Agent-to-Agent Communication”

In multi-agent systems, one agent’s output becomes another agent’s input. This is where structured output becomes especially critical. When Agent A delegates a task to Agent B, defining both the task specification and the expected result format as JSON Schemas turns the interface between agents into a typed contract — unambiguous and machine-verifiable.

# Agent-to-agent communication spec
subtask_spec = {
"task_id": "analyze_security_001",
"type": "security_analysis",
"input": {
"file_path": "/workspace/auth.py",
"focus_areas": ["sql_injection", "auth_bypass"]
},
"expected_output_schema": {
"vulnerabilities": [
{
"type": "string",
"severity": "high|medium|low",
"line": "integer",
"description": "string",
"recommendation": "string"
}
],
"overall_risk": "high|medium|low|safe"
}
}

When spec and result format are defined in advance, the receiving agent can consume the delegated agent’s output directly, without any fragile string parsing. The next chapter assembles everything covered so far into a minimal working agent loop in under 50 lines of Python.

References