langchain-ai/deepagents · error · TypeError

Invalid skill source: expected str or (str, str) tuple, got

Error message

Invalid skill source: expected str or (str, str) tuple, got {source!r}

What it means

Skill sources may be given as a plain path string or a (path, label) two-element string tuple. `_validate_tuple_source` rejects anything else — wrong tuple length, non-string elements, or other types — with a TypeError describing the received value. This keeps path/label bookkeeping (self.sources / self.source_labels) index-aligned.

Source

Thrown at libs/deepagents/deepagents/middleware/skills.py:175

name). The label is rendered as `**{label} Skills**` in the system
prompt; do not include the trailing "Skills" yourself.
"""


def _validate_tuple_source(source: tuple[object, ...]) -> None:
    """Raise `TypeError` if a tuple source is not a `(str, str)` pair.

    Catches the near-miss shapes at construction time so the traceback
    points at the caller rather than at a later `IndexError` inside the
    middleware or a silently-coerced non-string path downstream.
    """
    if (
        len(source) != 2  # noqa: PLR2004  # SkillSource tuple is exactly (path, label)
        or not isinstance(source[0], str)
        or not isinstance(source[1], str)
    ):
        msg = f"Invalid skill source: expected str or (str, str) tuple, got {source!r}"
        raise TypeError(msg)


def _source_path(source: SkillSource) -> str:
    """Return just the path component of a source."""
    if isinstance(source, str):
        return source
    _validate_tuple_source(source)
    return source[0]


def _truncate_skill_load_warning(error: str) -> str:
    """Cap a skill loading warning before placing it in the model prompt."""
    if len(error) <= MAX_SKILL_LOAD_WARNING_LENGTH:
        return error
    length = MAX_SKILL_LOAD_WARNING_LENGTH - len(_SKILL_LOAD_WARNING_TRUNCATION_SUFFIX)
    return f"{error[:length]}{_SKILL_LOAD_WARNING_TRUNCATION_SUFFIX}"

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Use str(pathlib_path) instead of a Path object.
  2. Ensure each tuple is exactly (str_path, str_label), e.g. ("/skills", "team-skills").
  3. Pass a plain string if no custom label is needed.
  4. Validate config-derived sources with a quick check before constructing the middleware.

Example fix

// before
sources=[Path("./skills")]
// after
sources=[str(Path("./skills")), ]  # or ("./skills", "local")
Defensive patterns

Strategy: type-guard

Validate before calling

def norm_source(s):
    if isinstance(s, str):
        return s
    if isinstance(s, tuple) and len(s) == 2 and all(isinstance(x, str) for x in s):
        return s
    raise TypeError(f"bad skill source: {s!r}")
sources = [norm_source(s) for s in cfg_sources]

Type guard

def is_skill_source(v: object) -> TypeGuard[str | tuple[str, str]]:
    if isinstance(v, str):
        return True
    return isinstance(v, tuple) and len(v) == 2 and all(isinstance(x, str) for x in v)

Try / catch

try:
    mw = SkillsMiddleware(backend=b, sources=sources)
except TypeError as e:
    if "Invalid skill source" in str(e):
        mw = SkillsMiddleware(backend=b, sources=[str(s) for s in sources])
    else:
        raise

Prevention

When it happens

Trigger: SkillsMiddleware(..., sources=[("/skills",)]) (length-1 tuple); [("/skills", 42)]; passing a Path object or a 3-tuple.

Common situations: Using pathlib.Path instead of str; packing an extra metadata field into the tuple; YAML/JSON config decoding tuples as lists of mixed types.

Related errors


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