tinyhumansai/openhuman · warning

missing required field `op`

Error message

missing required field `op`

What it means

The `todo` board tool was invoked without a usable `op` string (todo.rs:121): args["op"] is absent or not a JSON string. op selects the dispatch arm add|edit|update_status|remove|replace|clear|list; an unknown-but-present op string is a different, soft ToolResult::error listing the expected ops.

Source

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

                "cards": {
                    "type": "array",
                    "description": "Full card list for op=replace.",
                    "items": { "type": "object" }
                }
            },
            "required": ["op"]
        })
    }

    fn permission_level(&self) -> PermissionLevel {
        PermissionLevel::None
    }

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
        let op = args
            .get("op")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("missing required field `op`"))?
            .trim()
            .to_string();

        let location = current_location();
        tracing::debug!(op = %op, thread_id = ?location.thread_id(), "[tool][todo] dispatch");

        let result = match op.as_str() {
            "add" => {
                let content = required_string(&args, "content")?;
                let mut patch = patch_from_args(&args)?;
                if patch.approval_mode.is_none() {
                    patch.approval_mode = Some(default_task_approval_mode().await);
                }
                ops::add(&location, &content, patch).await
            }
            "edit" => {
                let id = required_string(&args, "id")?;
                let mut patch = patch_from_args(&args)?;

View on GitHub (pinned to a221052e0d)

Solutions

  1. Include "op" as one of add|edit|update_status|remove|replace|clear|list
  2. Check for key-name drift (operation/action instead of op)
  3. Validate args against the tool schema (required: ["op"]) before dispatch

Example fix

// before
{ "content": "Ship the fix" }

// after
{ "op": "add", "content": "Ship the fix" }
Defensive patterns

Strategy: type-guard

Validate before calling

const TODO_OPS = new Set(["add","edit","update_status","remove","replace","clear","list"]);
const a = args as Record<string, unknown>;
if (typeof a?.op !== "string" || !TODO_OPS.has(a.op)) {
  throw new Error(`todo: 'op' must be one of ${[...TODO_OPS].join("|")}`);
}

Type guard

const TODO_OPS = new Set(["add","edit","update_status","remove","replace","clear","list"]);
function isTodoDispatch(a: unknown): a is { op: string } {
  const op = (a as Record<string, unknown>)?.op;
  return typeof op === "string" && TODO_OPS.has(op);
}

Try / catch

if let Err(e) = tool.execute(args).await {
    if e.to_string().contains("missing required field `op`") {
        return Ok(ToolResult::error("todo requires 'op' (add|edit|update_status|remove|replace|clear|list)"));
    }
    return Err(e);
}

Prevention

When it happens

Trigger: Model omits op entirely; op passed as null or a non-string; wrapper calling the tool with only per-op fields (content, id) and no op.

Common situations: Models assuming a default op; argument builders keyed by operation without embedding the op field.

Related errors


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