tinyhumansai/openhuman · warning

Missing 'plan' parameter

Error message

Missing 'plan' parameter

What it means

The `plan_exit` tool was invoked without a `plan` string (plan_exit.rs:69). The tool's only job is to emit PLAN_EXIT_MARKER followed by the trimmed plan text, so the argument is the entire payload. The empty-after-trim case is a separate soft ToolResult::error; this error means args["plan"] is absent or not a string.

Source

Thrown at src/openhuman/agent/tools/plan_exit.rs:69

            "properties": {
                "plan": {
                    "type": "string",
                    "description": "Markdown-formatted plan text to hand off."
                }
            },
            "required": ["plan"]
        })
    }

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

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
        let plan = args
            .get("plan")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'plan' parameter"))?;
        let trimmed = plan.trim();
        if trimmed.is_empty() {
            return Ok(ToolResult::error("`plan` must not be empty"));
        }
        Ok(ToolResult::success(format!(
            "{PLAN_EXIT_MARKER}\n{trimmed}"
        )))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn plan_exit_emits_marker() {
        let tool = PlanExitTool::new();
        let result = tool

View on GitHub (pinned to a221052e0d)

Solutions

  1. Pass "plan" as the full plan text string
  2. If the plan is naturally a list, join it into one string before calling
  3. Validate args against the schema (required: ["plan"]) in any wrapper

Example fix

// before
{}

// after
{ "plan": "1. Reproduce\n2. Fix null check in loader\n3. Add regression test" }
Defensive patterns

Strategy: validation

Validate before calling

const a = args as Record<string, unknown>;
if (typeof a?.plan !== "string") {
  throw new Error("plan_exit: 'plan' string is required");
}

Type guard

function isPlanExitArgs(a: unknown): a is { plan: string } {
  return typeof (a as Record<string, unknown>)?.plan === "string"
      && ((a as { plan: string }).plan.trim().length > 0);
}

Try / catch

// plan_exit is terminal for the plan loop — feed a correctable error back
if e.to_string().contains("Missing 'plan'") {
    return Ok(ToolResult::error("call plan_exit with the final plan as the 'plan' string"));
}

Prevention

When it happens

Trigger: Model calls plan_exit with no arguments or an empty object; plan passed as an array of steps instead of a string; wrapper dropping the field.

Common situations: Plan-review flows where the model assumes the plan is carried in context; argument-shape drift after prompt changes.

Related errors


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