langchain-ai/deepagents · error · TypeError

`{field_name}` keys and values must be strings

Error message

`{field_name}` keys and values must be strings

What it means

After confirming the value is a Mapping, _coerce_str_mapping checks that every key and value is a str. Any non-string key or value raises a TypeError naming the field. This keeps tool-description override tables strictly str->str.

Source

Thrown at libs/deepagents/deepagents/profiles/harness/harness_profiles.py:838

    """Validate that `value` is a string or `None` for dict-loaded string fields."""
    if value is None or isinstance(value, str):
        return value
    msg = f"`{field_name}` must be str or None, got {type(value).__name__}"
    raise TypeError(msg)


def _coerce_str_mapping(value: object, field_name: str) -> dict[str, str]:
    """Validate that `value` is a `str -> str` mapping (or `None`) and return a plain dict."""
    if value is None:
        return {}
    if not isinstance(value, Mapping):
        msg = f"`{field_name}` must be a mapping, got {type(value).__name__}"
        raise TypeError(msg)
    out: dict[str, str] = {}
    for key, val in value.items():
        if not isinstance(key, str) or not isinstance(val, str):
            msg = f"`{field_name}` keys and values must be strings"
            raise TypeError(msg)
        out[key] = val
    return out


def _coerce_frozen_strset(value: object, field_name: str) -> frozenset[str]:
    """Validate that `value` is an iterable of strings (or `None`)."""
    if value is None:
        return frozenset()
    if not isinstance(value, (list, tuple, set, frozenset)):
        msg = f"`{field_name}` must be a list/set of strings, got {type(value).__name__}"
        raise TypeError(msg)
    entries: list[str] = []
    for entry in value:
        if not isinstance(entry, str):
            msg = f"`{field_name}` entries must be strings, got {type(entry).__name__} ({entry!r})"
            raise TypeError(msg)
        entries.append(entry)
    return frozenset(entries)

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Cast keys and values to str before passing them (str(key), str(value)).
  2. Fix the source data so override entries are string-to-string.
  3. Reject/repair the config upstream if keys are enums or ints, mapping them to official tool-name strings.

Example fix

// before
overrides = {ToolName.BASH: "Run shell"}
config = HarnessProfileConfig.from_dict({"tool_description_overrides": overrides})
// after
overrides = {str(ToolName.BASH.value): "Run shell"}
config = HarnessProfileConfig.from_dict({"tool_description_overrides": overrides})
Defensive patterns

Strategy: type-guard

Validate before calling

overrides = data.get("tool_description_overrides") or {}
if not all(isinstance(k, str) and isinstance(v, str) for k, v in overrides.items()):
    data["tool_description_overrides"] = {str(k): str(v) for k, v in overrides.items()}

Type guard

def is_str_mapping(value: object) -> TypeGuard[dict[str, str]]:
    return isinstance(value, Mapping) and all(
        isinstance(k, str) and isinstance(v, str) for k, v in value.items()
    )

Try / catch

try:
    config = HarnessProfileConfig.from_dict(data)
except TypeError as e:
    if "keys and values must be strings" in str(e):
        field = str(e).split('`')[1]
        data[field] = {str(k): str(v) for k, v in data[field].items()}
        config = HarnessProfileConfig.from_dict(data)
    else:
        raise

Prevention

When it happens

Trigger: from_dict/to_dict/from_harness_profile receiving tool_description_overrides like {"bash": 42} (non-str value) or {42: "desc"} (non-str key), e.g. from JSON with numeric keys or enums as keys.

Common situations: JSON object keys serialized from numeric tool IDs; YAML configs where a description was written as a number or boolean; programmatic overrides built with enum or non-str tool names.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/f328c154dda104e5. Report an issue: GitHub.