FoundationAgents/OpenManus · warning · ToolError

Parameter `plan_id` is required for command: update

Error message

Parameter `plan_id` is required for command: update

What it means

Raised by _update_plan (app/tool/planning.py:165) when command='update' is dispatched without a truthy plan_id. Unlike get/mark_step, update has no fallback to the currently active plan — the target plan must be identified explicitly. Validation happens before any mutation, so no partial writes occur.

Source

Thrown at app/tool/planning.py:165

            "title": title,
            "steps": steps,
            "step_statuses": ["not_started"] * len(steps),
            "step_notes": [""] * len(steps),
        }

        self.plans[plan_id] = plan
        self._current_plan_id = plan_id  # Set as active plan

        return ToolResult(
            output=f"Plan created successfully with ID: {plan_id}\n\n{self._format_plan(plan)}"
        )

    def _update_plan(
        self, plan_id: Optional[str], title: Optional[str], steps: Optional[List[str]]
    ) -> ToolResult:
        """Update an existing plan with new title or steps."""
        if not plan_id:
            raise ToolError("Parameter `plan_id` is required for command: update")

        if plan_id not in self.plans:
            raise ToolError(f"No plan found with ID: {plan_id}")

        plan = self.plans[plan_id]

        if title:
            plan["title"] = title

        if steps:
            if not isinstance(steps, list) or not all(
                isinstance(step, str) for step in steps
            ):
                raise ToolError(
                    "Parameter `steps` must be a list of strings for command: update"
                )

            # Preserve existing step statuses for unchanged steps

View on GitHub (pinned to 52a13f2a57)

Solutions

  1. Pass the plan_id explicitly: `planning.execute(command='update', plan_id='p1', title='New title')`.
  2. In wrappers, resolve the active plan yourself first (planning._current_plan_id or a prior list/get call) and inject it for update calls.
  3. Keep the asymmetry documented in your agent prompt: update requires plan_id; get/mark_step fall back to the active plan.

Example fix

// before
await planning.execute(command='update', title='Revised')

// after
await planning.execute(command='update', plan_id='p1', title='Revised')
Defensive patterns

Strategy: validation

Validate before calling

plan_id = plan_id or current_active_id()  # resolve in caller; update has no fallback
if not plan_id:
    raise ValueError('update requires an explicit plan_id')
await planning.execute(command='update', plan_id=plan_id, title=title)

Type guard

def is_plan_id(v: str | None) -> bool:
    return isinstance(v, str) and bool(v.strip())

Try / catch

try:
    await planning.execute(command='update', title=t)
except ToolError as e:
    if 'plan_id` is required for command: update' in str(e):
        await planning.execute(command='update', plan_id=await resolve_active_id(), title=t)
    else:
        raise

Prevention

When it happens

Trigger: Calling `planning.execute(command='update', title='New title')` with plan_id omitted or ''. This is easy to hit because the sibling commands get and mark_step do default to the active plan, suggesting (incorrectly) that update would too.

Common situations: Models switching between planning commands and assuming uniform optional plan_id semantics; refactors that copy a get call and change only the command string.

Related errors


AI-assisted analysis of FoundationAgents/OpenManus@52a13f2a57 (2026-08-15). Data as JSON: /api/errors/19c485383ef68bc4. Report an issue: GitHub.