Tool-message JSON pitfalls in Letta imports (and how we fixed them)

Tool-message JSON pitfalls in Letta imports (and how we fixed them)

A routine Letta migration crashed one of our servers last month. The .af file imported cleanly. The agent looked fine. Then the API started returning HTTP 500 every time the conversation crossed a particular message. The cause was tool-message JSON that Letta Cloud had stored as plain English — valid on the way in, fatal on the way out.

Letta exports can include a tool message (tool_message) alongside assistant_message and reasoning_message; importers that drop or mis-parse that JSON shape are what bricked our first migration attempts.

What Happened

We were migrating a Letta deployment with 23 agents from staging to production. The .af export looked clean. The import ran. Twelve agents succeeded. Eleven failed silently — Letta accepted them but they didn't respond to queries. Then the server crashed entirely during an unrelated operation.

Post-mortem revealed: malformed tool-message JSON in the agent definitions. Some agents had tool configurations that parsed as valid JSON but violated Letta's expected structure. Letta accepted them but they created unstable runtime states. The crash was unrelated but triggered by the cascading instability.

This is the story of what went wrong, how we found it, and how we fixed it.

The Tool-Message JSON Expected Structure

Letta agent definitions include tool configurations. These are defined in JSON with a specific structure:

 {
 "name": "web_search",
 "description": "Search the web for information",
 "parameters": {
 "type": "object",
 "properties": {
 "query": {
 "type": "string",
 "description": "The search query"
 }
 },
 "required": ["query"]
 }
 }

That's the expected format. Letta validates against this structure when loading tools. If the structure matches, the tool loads and functions correctly.

What We Found

We reviewed the failed imports. Here's what was wrong:

Pitfall One: Missing Required Fields

 {
 "name": "web_search",
 "description": "Search for info"
 // Missing "parameters" entirely
 }

This tool parsed as valid JSON but lacked the required "parameters" field. Letta accepted it but couldn't invoke the tool properly. The agent loaded but tool calls failed silently.

Pitfall Two: Incorrect Type Values

 {
 "name": "web_search",
 "description": "Search for info",
 "parameters": {
 "type": "object",
 "properties": {
 "query": {
 "type": "stringtext",  // Wrong - should be "string"
 "description": "Search query"
 }
 }
 }
 }

A typo in the type value. Valid JSON, but "stringtext" isn't a recognized JSON Schema type. Letta accepted it but couldn't validate parameters correctly.

Pitfall Three: Mismatched Required Arrays

 {
 "name": "web_search",
 "description": "Search for info",
 "parameters": {
 "type": "object",
 "properties": {
 "query": { "type": "string" }
 },
 "required": ["query", "limit"]  // "limit" isn't defined
 }
 }

The required array referenced a property that didn't exist. Letta's validation passed but runtime failed when calling the tool.

Pitfall Four: Null vs. Missing

 {
 "name": "web_search",
 "description": "Search for info",
 "parameters": null  // Should be an object, not null
 }

Null values instead of objects. Valid JSON, but not what Letta expects.

How We Identified the Problems

We built a validation script:

 import json

def validate_tool_definition(tool_json: dict) -> list:
 """Validate a tool definition and return list of errors."""
 errors = []

# Check required top-level fields
 required = ['name', 'description', 'parameters']
 for field in required:
 if field not in tool_json:
 errors.append(f"Missing required field: {field}")

# Validate parameters structure
 if 'parameters' in tool_json:
 params = tool_json['parameters']

if params is None:
 errors.append("parameters cannot be null")
 elif not isinstance(params, dict):
 errors.append(f"parameters must be object, got {type(params)}")
 else:
 # Check type field
 if 'type' not in params:
 errors.append("parameters missing 'type' field")
 elif params['type'] != 'object':
 errors.append(f"parameters 'type' must be 'object', got {params['type']}")

# Check properties if present
 if 'properties' in params:
 props = params['properties']
 if not isinstance(props, dict):
 errors.append(f"properties must be object, got {type(props)}")

# Validate required against properties
 if 'required' in params:
 required_props = params['required']
 if isinstance(required_props, list):
 for req in required_props:
 if req not in props:
 errors.append(
 f"required references undefined: {req}"
 )

return errors

def validate_agent_file(agent_json_path: str) -> None:
 """Validate an entire agent file."""
 with open(agent_json_path) as f:
 agent = json.load(f)

# Check tools section
 tools = agent.get('tools', [])
 print(f"Validating {len(tools)} tools...")

for i, tool in enumerate(tools):
 errors = validate_tool_definition(tool)
 if errors:
 print(f"Tool {i} ({tool.get('name', 'UNKNOWN')}):")
 for error in errors:
 print(f"  - {error}")
 else:
 print(f"Tool {i}: OK")

We ran this against every exported .af file before import. The validation caught every malformed tool definition we had.

How We Fixed It

We corrected the three categories of problems:

Fix One: Add Missing Fields

 {
 "name": "web_search",
 "description": "Search the web for information",
 "parameters": {
 "type": "object",
 "properties": {
 "query": {
 "type": "string",
 "description": "The search query"
 }
 },
 "required": ["query"]
 }
 }

Added missing required fields. Simple but necessary.

Fix Two: Correct Type Values

 "query": {
 "type": "string",  // Changed from "stringtext"
 "description": "The search query"
 }

Fixed typoes in type values. Letta's JSON Schema validation expects specific values.

Fix Three: Align Required Arrays

 "required": ["query"]  // Removed "limit" - wasn't in properties

Ensured required arrays matched actual properties. This required manual verification.

The Validation Pipeline

We built validation into the .af workflow:

 # Before importing, validate everything
 broca validate my-agent.af

Broca now includes per-export validation. It checks tool-message JSON, validates structure, and reports problems before attempting import. This prevents the crash-and-recovery scenario we experienced.

The validation runs automatically on import, too:

 # Import with validation
 broca import my-agent.af --target http://letta-target --validate

That flag runs validation before importing. If you have problems, the import stops and reports them.

What We Learned

Here's what migration taught us:

JSON-valid is not Letta-valid. Valid JSON parses correctly but doesn't match Letta's schema. Always validate against expected structure.

Import failures can be silent. Letta accepted malformed agents but they didn't work. Check agent responses after import.

Validation must be proactive. Don't discover problems at runtime. Validate during export, before import, and during import.

Schema documentation matters. We needed to know what Letta expected. That's now in our documentation.

The Fix in Practice

After the fixes, our migration workflow became:

 # Export
 broca export agent-abc123 --output agent.af

# Validate before import
 broca validate agent.af

# Import with validation
 broca import agent.af --target http://letta-target --validate

That's three commands, and the middle one catches problems. That's saved us multiple times since.

Close

The tool-message JSON problem cost us an afternoon and a scary server crash. But it also produced validation tooling that's prevented subsequent failures.

The lesson: migration isn't just about moving data. It's about validating what's in that data. Letta accepts what looks reasonable but crashes when it tries to use what isn't.

Validate everything. Then validate again.

If you're doing Letta migrations, run validation. We can share our validation script or help you integrate it into your workflow. Reach out.

If you are weighing build-vs-buy on infrastructure like this—and the real question is what to commit to next—describe the decision you are facing. We scope around outcomes, not open-ended tours.