{"record":{"id":"614ebc372bf7b620","repo":"tinyhumansai/openhuman","slug":"missing-required-field-op","errorCode":null,"errorMessage":"missing required field `op`","messagePattern":"missing required field `op`","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/openhuman/agent/tools/todo.rs","lineNumber":121,"sourceCode":"                \"cards\": {\n                    \"type\": \"array\",\n                    \"description\": \"Full card list for op=replace.\",\n                    \"items\": { \"type\": \"object\" }\n                }\n            },\n            \"required\": [\"op\"]\n        })\n    }\n\n    fn permission_level(&self) -> PermissionLevel {\n        PermissionLevel::None\n    }\n\n    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {\n        let op = args\n            .get(\"op\")\n            .and_then(|v| v.as_str())\n            .ok_or_else(|| anyhow::anyhow!(\"missing required field `op`\"))?\n            .trim()\n            .to_string();\n\n        let location = current_location();\n        tracing::debug!(op = %op, thread_id = ?location.thread_id(), \"[tool][todo] dispatch\");\n\n        let result = match op.as_str() {\n            \"add\" => {\n                let content = required_string(&args, \"content\")?;\n                let mut patch = patch_from_args(&args)?;\n                if patch.approval_mode.is_none() {\n                    patch.approval_mode = Some(default_task_approval_mode().await);\n                }\n                ops::add(&location, &content, patch).await\n            }\n            \"edit\" => {\n                let id = required_string(&args, \"id\")?;\n                let mut patch = patch_from_args(&args)?;","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/openhuman/agent/tools/todo.rs#L103-L139","documentation":"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.","triggerScenarios":"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.","commonSituations":"Models assuming a default op; argument builders keyed by operation without embedding the op field.","solutions":["Include \"op\" as one of add|edit|update_status|remove|replace|clear|list","Check for key-name drift (operation/action instead of op)","Validate args against the tool schema (required: [\"op\"]) before dispatch"],"exampleFix":"// before\n{ \"content\": \"Ship the fix\" }\n\n// after\n{ \"op\": \"add\", \"content\": \"Ship the fix\" }","handlingStrategy":"type-guard","validationCode":"const TODO_OPS = new Set([\"add\",\"edit\",\"update_status\",\"remove\",\"replace\",\"clear\",\"list\"]);\nconst a = args as Record<string, unknown>;\nif (typeof a?.op !== \"string\" || !TODO_OPS.has(a.op)) {\n  throw new Error(`todo: 'op' must be one of ${[...TODO_OPS].join(\"|\")}`);\n}","typeGuard":"const TODO_OPS = new Set([\"add\",\"edit\",\"update_status\",\"remove\",\"replace\",\"clear\",\"list\"]);\nfunction isTodoDispatch(a: unknown): a is { op: string } {\n  const op = (a as Record<string, unknown>)?.op;\n  return typeof op === \"string\" && TODO_OPS.has(op);\n}","tryCatchPattern":"if let Err(e) = tool.execute(args).await {\n    if e.to_string().contains(\"missing required field `op`\") {\n        return Ok(ToolResult::error(\"todo requires 'op' (add|edit|update_status|remove|replace|clear|list)\"));\n    }\n    return Err(e);\n}","preventionTips":["Always dispatch with an explicit op — never rely on a default","Validate op against the closed set before invoking","Keep the unknown-op soft error's list and the schema in sync"],"tags":["rust","tool","arguments","llm","todo"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}