NousResearch/hermes-agent · error · SubagentLifecycleError
metadata must be JSON-serializable.
Error message
metadata must be JSON-serializable.
What it means
Validation error from SubagentLifecycleManager._validate_request(): request.metadata must be JSON-serializable because the manager json.dumps()s it (sorted keys) both to measure size and to store it. If serialization raises TypeError/ValueError (sets, custom objects, datetime, recursive refs), the launch is rejected.
Source
Thrown at agent/subagent_lifecycle.py:523
raise SubagentLifecycleError("role must be 'leaf' or 'orchestrator'.")
if request.timeout_seconds is not None:
raise SubagentLifecycleError(
"Per-launch timeout is not supported; configure delegation timeout explicitly."
)
if request.working_directory is not None:
raise SubagentLifecycleError(
"working_directory is not supported because Hermes delegates use isolated task environments."
)
if request.blocked_tools:
raise SubagentLifecycleError(
"Per-tool blocking is not supported; use allowed_toolsets. Hermes always blocks unsafe child tools."
)
try:
metadata_bytes = len(
json.dumps(dict(request.metadata), sort_keys=True).encode()
)
except (TypeError, ValueError) as exc:
raise SubagentLifecycleError("metadata must be JSON-serializable.") from exc
if metadata_bytes > _MAX_METADATA_BYTES:
raise SubagentLifecycleError("metadata exceeds 8192 bytes.")
if request.allowed_toolsets:
from toolsets import TOOLSETS
unknown = set(request.allowed_toolsets) - set(TOOLSETS)
if unknown:
raise SubagentLifecycleError(
f"Unknown toolsets: {', '.join(sorted(unknown))}."
)
enabled = getattr(parent, "enabled_toolsets", None)
if enabled is not None and not set(request.allowed_toolsets).issubset(
set(enabled)
):
raise SubagentLifecycleError(
"Requested toolsets would broaden parent permissions."
)
View on GitHub (pinned to c896c09c42)
Solutions
- Pre-serialize metadata yourself: metadata = json.loads(json.dumps(metadata, default=str)) to coerce or fail fast with a clear error.
- Convert known non-JSON types explicitly (str(uuid), isoformat() for datetimes, sorted(list(s)) for sets).
- Keep metadata to plain str/int/float/bool/None/list/dict composed of the same.
Example fix
# before
request = SubagentLaunchRequest(goal=g, metadata={"tags": {"a", "b"}, "at": datetime.now()})
# after
request = SubagentLaunchRequest(
goal=g,
metadata={"tags": ["a", "b"], "at": datetime.now().isoformat()},
) Defensive patterns
Strategy: validation
Validate before calling
import json
try:
json.dumps(metadata, sort_keys=True)
except (TypeError, ValueError):
metadata = json.loads(json.dumps(metadata, default=str)) # or fix at source
request = SubagentLaunchRequest(goal=goal, metadata=metadata) Type guard
import json
def is_json_serializable(value: object) -> bool:
try:
json.dumps(value, sort_keys=True)
except (TypeError, ValueError):
return False
return True Prevention
- Keep metadata to plain JSON types (str/int/float/bool/None/list/dict).
- Convert datetime->isoformat, UUID->str, set->sorted list before launch.
- Round-trip metadata through json.dumps(default=str) as a final sanitizer.
When it happens
Trigger: Passing metadata containing set(), datetime objects, dataclass instances, numpy types, or objects without a JSON representation; metadata values coming straight from ORM/model objects; circular references in nested dicts.
Common situations: Stuffing ORM rows or pydantic-adjacent objects into metadata; timestamp fields left as datetime instead of ISO strings; third-party types (Decimal, UUID, tuples are fine in json but sets are not) leaking in from config parsing.
Related errors
- metadata exceeds 8192 bytes.
- parent_session_id does not match the active session.
- goal must be a non-empty string of at most 16000 characters.
- context must be a string of at most 32000 characters.
- role must be 'leaf' or 'orchestrator'.
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/d8f9cb47b613ad25.
Report an issue: GitHub.