NousResearch/hermes-agent · error · SubagentLifecycleError
Malformed subagent handle.
Error message
Malformed subagent handle.
What it means
Raised by SubagentHandle.from_dict() in agent/subagent_lifecycle.py when the input mapping cannot construct the frozen dataclass — wrong field names, extra keys, missing fields, or wrong value types cause cls(**dict(value)) to raise TypeError/ValueError. It protects the public serialized-handle contract for subagent handles.
Source
Thrown at agent/subagent_lifecycle.py:86
subagent_id: str
parent_session_id: Optional[str]
correlation_id: Optional[str]
created_at: float
provider: Optional[str]
model: Optional[str]
role: str
depth: int
capability: str
def to_dict(self) -> dict[str, Any]:
return dataclasses.asdict(self)
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "SubagentHandle":
try:
return cls(**dict(value))
except (TypeError, ValueError) as exc:
raise SubagentLifecycleError("Malformed subagent handle.") from exc
@dataclasses.dataclass(frozen=True)
class SubagentStatus:
handle: SubagentHandle
state: SubagentState
updated_at: float
diagnostic: Optional[str] = None
@dataclasses.dataclass(frozen=True)
class SubagentTerminalState:
handle: SubagentHandle
state: SubagentState
completed: bool
timed_out: bool = False
diagnostic: Optional[str] = None
View on GitHub (pinned to c896c09c42)
Solutions
- Regenerate the handle via SubagentHandle.to_dict() from the same Hermes version that will consume it — do not hand-write the mapping.
- If deserializing stored handles, verify the dict keys exactly match the dataclass fields (contract_version, subagent_id, parent_session_id, correlation_id, created_at, provider, model, role, depth, capability) and coerce value types.
- Check PUBLIC_CONTRACT_VERSION before from_dict() and discard/re-launch handles from mismatched versions.
- If the handle came from your own code, log the raw dict and diff its keys against the dataclass fields.
Example fix
# before
handle = SubagentHandle.from_dict(loaded_json["handle"])
# after
raw = loaded_json["handle"]
expected = {f.name for f in dataclasses.fields(SubagentHandle)}
if int(raw.get("contract_version", -1)) != PUBLIC_CONTRACT_VERSION or set(raw) != expected:
raise ValueError("stored handle does not match current contract; re-launch subagent")
handle = SubagentHandle.from_dict(raw) Defensive patterns
Strategy: type-guard
Validate before calling
import dataclasses
from agent.subagent_lifecycle import SubagentHandle, PUBLIC_CONTRACT_VERSION
FIELDS = {f.name for f in dataclasses.fields(SubagentHandle)}
def is_valid_handle_dict(value) -> bool:
return (
isinstance(value, dict)
and set(value) == FIELDS
and value.get("contract_version") == PUBLIC_CONTRACT_VERSION
and isinstance(value.get("depth"), int)
and isinstance(value.get("created_at"), (int, float))
) Type guard
def is_subagent_handle_dict(value: object) -> bool:
import dataclasses
if not isinstance(value, dict):
return False
names = {f.name for f in dataclasses.fields(SubagentHandle)}
return set(value.keys()) == names Try / catch
try:
handle = SubagentHandle.from_dict(raw)
except SubagentLifecycleError:
handle = None # stale/corrupt handle: re-launch instead of reusing Prevention
- Only round-trip handles via to_dict()/from_dict() within the same Hermes version.
- Never hand-construct handle dicts; always use launch() and serialize its return value.
- Store PUBLIC_CONTRACT_VERSION alongside the handle and discard on mismatch.
When it happens
Trigger: Deserializing a handle dict produced by a different (older/newer) contract version; hand-building a dict with typos or extra keys; passing a JSON blob where nested values (e.g. depth) are strings instead of ints; persisting a handle across a Hermes upgrade and reloading it.
Common situations: Plugin code storing handle.to_dict() in a database or file and reading it back after Hermes updates the handle schema; inter-process handoff where the dict was re-encoded and types changed (int -> str).
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- No active Hermes parent session is available.
- parent_session_id does not match the active session.
- Duplicate correlation_id for this parent session.
- Hermes failed to assign a child identity.
- goal must be a non-empty string of at most 16000 characters.
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/0e7d0feb3a9b22d3.
Report an issue: GitHub.