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

  1. Regenerate the handle via SubagentHandle.to_dict() from the same Hermes version that will consume it — do not hand-write the mapping.
  2. 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.
  3. Check PUBLIC_CONTRACT_VERSION before from_dict() and discard/re-launch handles from mismatched versions.
  4. 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

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

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/0e7d0feb3a9b22d3. Report an issue: GitHub.