tinyhumansai/openhuman · warning

invalid `cards`: {e}

Error message

invalid `cards`: {e}

What it means

op=replace found `cards` but serde_json could not deserialize it into Vec<TaskBoardCard> (todo.rs:158). The {e} chain names the exact mismatch: cards is not a JSON array, or an element is missing a required card field or has a wrong type. Distinct from the missing-cards error, which fires only when the key is absent.

Source

Thrown at src/openhuman/agent/tools/todo.rs:158

                patch.content = optional_string(&args, "content");
                ops::edit(&location, &id, patch).await
            }
            "update_status" => {
                let id = required_string(&args, "id")?;
                let status = required_string(&args, "status")?;
                let status = ops::parse_status(&status).map_err(anyhow::Error::msg)?;
                ops::update_status(&location, &id, status).await
            }
            "remove" => {
                let id = required_string(&args, "id")?;
                ops::remove(&location, &id).await
            }
            "replace" => {
                let cards = args
                    .get("cards")
                    .ok_or_else(|| anyhow::anyhow!("missing `cards` for op=replace"))?;
                let cards: Vec<TaskBoardCard> = serde_json::from_value(cards.clone())
                    .map_err(|e| anyhow::anyhow!("invalid `cards`: {e}"))?;
                ops::replace(&location, cards).await
            }
            "clear" => ops::clear(&location).await,
            "list" => ops::list(&location).await,
            other => {
                return Ok(ToolResult::error(format!(
                "unknown op '{other}' (expected add|edit|update_status|remove|replace|clear|list)"
            )))
            }
        };

        match result {
            Ok(snap) => {
                let payload = json!({
                    "threadId": snap.thread_id,
                    "cards": snap.cards,
                    "markdown": snap.markdown,
                });

View on GitHub (pinned to a221052e0d)

Solutions

  1. Make cards a JSON array of card objects matching TaskBoardCard's required fields and types
  2. Read the {e} message — serde names the offending field and expected type precisely
  3. Round-trip validate with serde_json::from_value::<Vec<TaskBoardCard>> in wrappers before invoking the tool

Example fix

// before
{ "op": "replace", "cards": { "t1": { "content": "Ship" } } }

// after
{ "op": "replace", "cards": [{ "id": "t1", "content": "Ship the fix" }] }
Defensive patterns

Strategy: validation

Validate before calling

// Rust — pre-flight the exact deserialization the tool will perform
let cards: Vec<TaskBoardCard> = serde_json::from_value(args["cards"].clone())
    .context("cards payload does not match TaskBoardCard shape")?;

Type guard

function isCardArray(v: unknown): v is Array<Record<string, unknown>> {
  return Array.isArray(v) && v.every(c => typeof c === "object" && c !== null && "id" in c);
}

Try / catch

// Surface serde's precise field error to the model for self-correction
if e.to_string().contains("invalid `cards`") {
    return Ok(ToolResult::error(format!("cards shape invalid: {e:#}} — see card schema")));
}

Prevention

When it happens

Trigger: cards passed as an object or map keyed by id instead of an array; card elements missing required fields (e.g. id/content) or carrying wrong JSON types (numbers for string fields); null cards (deserialization failure, not the missing-key path).

Common situations: Models echoing the board shape from memory instead of the schema; wrappers forwarding a UI state object whose card fields were renamed.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/60929e4d5d509d14. Report an issue: GitHub.