langchain-ai/deepagents · error · ValueError

a failed write must carry an error detail

Error message

a failed write must carry an error detail

What it means

WriteResult's `__post_init__` (writer.py) enforces outcome consistency: a result with ok=False must carry an `error` detail explaining the failure. Returning a bare failure with no error would leave callers and users without any diagnosis, so it is rejected at construction time.

Source

Thrown at libs/code/deepagents_code/configuration/writer.py:44

    ok: bool
    changed: bool
    error: str | None = None

    def __post_init__(self) -> None:
        """Reject outcomes that cannot describe a real transaction.

        Callers branch on `ok` alone, so a failure with no detail would surface
        as a bare "could not be saved" with nothing to act on, and a change
        recorded against a failed write would report an edit that never
        reached the file.

        Raises:
            ValueError: If the three fields do not describe one outcome.
        """
        if not self.ok and self.error is None:
            msg = "a failed write must carry an error detail"
            raise ValueError(msg)
        if self.changed and not self.ok:
            msg = "a failed write cannot have changed the file"
            raise ValueError(msg)
        if self.ok and self.error is not None:
            msg = "a successful write cannot carry an error detail"
            raise ValueError(msg)


def update_user_config(
    mutate: Callable[[dict[str, Any]], bool],
    *,
    config_path: Path | None = None,
) -> WriteResult:
    """Serialize a read-modify-write of the user config and replace it atomically.

    Writes the user tier only. The managed path is refused rather than trusted
    to be unreachable.

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Populate `error` with a descriptive message whenever ok=False (e.g. str(exc) or a specific reason)
  2. Where an exception is caught, convert it to the error field: WriteResult(ok=False, error=str(exc), changed=False)
  3. If the write actually succeeded, set ok=True instead of ok=False with no error

Example fix

// before
WriteResult(ok=False, error=None, changed=False)
// after
WriteResult(ok=False, error="permission denied writing ~/.config/deepagents/config.toml", changed=False)
Defensive patterns

Strategy: try-catch

Validate before calling

def check_write_result(ok: bool, error: str | None) -> str | None:
    if not ok and error is None:
        return "failed write requires an error detail"
    return None

issue = check_write_result(ok, err)  # check before constructing

Type guard

def is_valid_write_result(ok: bool, error: str | None) -> bool:
    return ok or error is not None

Try / catch

try:
    result = WriteResult(ok=False, error=None, changed=False)
except ValueError:
    result = WriteResult(ok=False, error="unknown write failure", changed=False)

Prevention

When it happens

Trigger: Constructing WriteResult(ok=False, error=None, ...) directly — e.g. building the result in custom tooling/tests, or a code path that catches an exception but forgets to convert it to an error string before constructing the result.

Common situations: Wrapping update_user_config in tests with hand-built results; a custom mutate callable wrapper that swallows exceptions and returns ok=False without populating error; refactoring that dropped the error field population.

Related errors


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