20 KiB
Next Actions Model Refactor Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Move MCP workflow guidance UX into reusable NextActions models so wrapper draft guidance stops living as ad-hoc dict helpers in handlers.py.
Architecture: Add src/wf_mcp/workflow_surface/next_actions.py as the single owner of next-action enums, response models, and the first constructor: NextActions.from_wrapper_hints(...). Keep the existing create_draft_workspace_from_capability JSON stable, with can_continue as the only additive field. Do not add deployment/run guidance in this pass; this is the foundation for those later constructors.
Tech Stack: Python 3.14, Pydantic v2, FastMCP schema generation, pytest, ruff, basedpyright.
File Structure
-
Create
src/wf_mcp/workflow_surface/next_actions.py- Owns
NextActionTool,NextActionPatchExample, andNextActions. - Owns wrapper-hint guidance policy currently in
handlers.py. - Contains docstrings explaining that
next_actionsis advisory, not authority.
- Owns
-
Modify
src/wf_mcp/workflow_surface/models.py- Reuse the generic models for MCP response schemas.
- Keep compatibility aliases for
WrapperDraftPatchExampleandWrapperDraftNextActionsif local imports or generated schemas still reference the old names.
-
Modify
src/wf_mcp/workflow_surface/handlers.py- Remove
_wrapper_draft_next_actions(...)and_wrapper_draft_patch_examples(...). - Call
NextActions.from_wrapper_hints(...).model_dump(mode="json").
- Remove
-
Create
tests/wf_mcp/workflow_surface/test_next_actions.py- Unit-test the generic model without the full draft store.
-
Modify
tests/wf_mcp/workflow_surface/test_drafts.py- Keep integration coverage through
create_draft_workspace_from_capability. - Assert old fields still exist and
can_continueis additive.
- Keep integration coverage through
-
Modify
tests/wf_mcp/server/test_config.py- Assert the MCP output schema documents
can_continueand still documentscan_save_now.
- Assert the MCP output schema documents
-
Modify
docs/workflow_capabilities.md- Add one short note that
next_actionsis advisory guidance and not validation authority.
- Add one short note that
Scope Boundaries
- Do not add
NextActions.from_deployment_validation(...)in this pass. - Do not add
NextActions.from_run_result(...)in this pass. - Do not change current wrapper patch example semantics.
- Do not infer MCP content block extraction or boolean routing.
- Do not make
can_save_nowauthoritative.
Task 1: Add Generic Next-Actions Unit Tests
Files:
-
Create:
tests/wf_mcp/workflow_surface/test_next_actions.py -
Step 1: Write failing tests for high-confidence wrapper hints
Create tests/wf_mcp/workflow_surface/test_next_actions.py:
from __future__ import annotations
from wf_mcp.workflow_surface.next_actions import NextActionTool, NextActions
def test_next_actions_from_high_confidence_wrapper_hints_can_validate() -> None:
actions = NextActions.from_wrapper_hints(
workspace_id="echo_workspace",
revision=3,
hints={
"confidence": "high",
"missing_decisions": [],
"notes": [],
},
)
dumped = actions.model_dump(mode="json")
assert dumped["can_continue"] is True
assert dumped["can_save_now"] is True
assert dumped["recommended_next_tool"] == (
NextActionTool.VALIDATE_DRAFT_WORKSPACE.value
)
assert "high confidence" in dumped["reason"]
assert dumped["patch_examples"] == []
assert dumped["warnings"] == []
- Step 2: Write failing tests for low-confidence wrapper hints
Append to tests/wf_mcp/workflow_surface/test_next_actions.py:
def test_next_actions_from_low_confidence_wrapper_hints_can_patch() -> None:
actions = NextActions.from_wrapper_hints(
workspace_id="echo_workspace",
revision=4,
hints={
"confidence": "low",
"missing_decisions": [
{
"kind": "review_nested_output",
"message": "Review nested output fields before mapping.",
},
{
"kind": "confirm_boolean_outcomes",
"message": "Boolean fields may be data, not outcomes.",
},
],
"notes": ["Raw MCP tool output is not workflow-shaped."],
},
)
dumped = actions.model_dump(mode="json")
assert dumped["can_continue"] is True
assert dumped["can_save_now"] is False
assert dumped["recommended_next_tool"] == NextActionTool.PATCH_DRAFT_WORKSPACE.value
assert "missing wrapper decisions" in dumped["reason"]
assert dumped["warnings"][0] == "Raw MCP tool output is not workflow-shaped."
assert len(dumped["patch_examples"]) == 2
assert dumped["patch_examples"][0]["tool"] == (
NextActionTool.PATCH_DRAFT_WORKSPACE.value
)
assert dumped["patch_examples"][0]["request"]["workspace_id"] == "echo_workspace"
assert dumped["patch_examples"][0]["request"]["revision"] == 4
assert dumped["patch_examples"][0]["request"]["patch"][0]["path"] == (
"/draft/steps/call/output"
)
assert dumped["patch_examples"][1]["request"]["patch"] == []
- Step 3: Run the focused test to verify it fails
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py -q
Expected: FAIL with ModuleNotFoundError: No module named 'wf_mcp.workflow_surface.next_actions'.
Task 2: Implement next_actions.py
Files:
-
Create:
src/wf_mcp/workflow_surface/next_actions.py -
Step 1: Create the generic models and wrapper-hints constructor
Create src/wf_mcp/workflow_surface/next_actions.py:
from __future__ import annotations
from enum import StrEnum
from typing import Any, Self
from pydantic import BaseModel, Field
from .wrapper_hints import WrapperAuthoringHints
class NextActionTool(StrEnum):
"""Stable MCP workflow tools that guidance may recommend."""
PATCH_DRAFT_WORKSPACE = "wf.workflow.patch_draft_workspace"
VALIDATE_DRAFT_WORKSPACE = "wf.workflow.validate_draft_workspace"
VALIDATE_DEPLOYMENT = "wf.workflow.validate_deployment"
RUN_DEPLOYMENT = "wf.workflow.run_deployment"
RESUME_RUN = "wf.workflow.resume_run"
READ_RUN_TRACE = "wf.workflow.read_run_trace"
class NextActionPatchExample(BaseModel):
"""Concrete example request for a recommended MCP workflow tool."""
description: str = Field(description="Human-readable reason for this example.")
tool: NextActionTool = Field(description="MCP workflow tool to call.")
request: dict[str, Any] = Field(
description="JSON request payload to pass to the tool."
)
class NextActions(BaseModel):
"""Advisory continuation hints for MCP workflow clients.
This object is guidance, not authority. Validation diagnostics and runtime
status remain the source of truth; clients should treat this as a compact
answer to "what tool should I call next?"
"""
can_continue: bool = Field(
description=(
"Whether there is an obvious next workflow-surface tool call. "
"Advisory only."
)
)
can_save_now: bool | None = Field(
default=None,
description=(
"Advisory wrapper-authoring signal. False means review is "
"recommended before saving; the server does not enforce this."
),
)
recommended_next_tool: NextActionTool | None = Field(
default=None,
description="Suggested next MCP workflow tool, if one is obvious.",
)
reason: str = Field(description="Short explanation for the recommendation.")
patch_examples: list[NextActionPatchExample] = Field(
default_factory=list,
description="Concrete JSON Patch examples for common missing decisions.",
)
warnings: list[str] = Field(
default_factory=list,
description="Non-blocking warnings copied from low-confidence hints.",
)
@classmethod
def from_wrapper_hints(
cls,
*,
workspace_id: str,
revision: int,
hints: WrapperAuthoringHints | dict[str, Any],
) -> Self:
"""Create guidance after bootstrapping a wrapper draft workspace."""
payload = (
hints.model_dump(mode="json") if isinstance(hints, WrapperAuthoringHints)
else hints
)
confidence = str(payload.get("confidence", "low"))
missing_decisions = payload.get("missing_decisions")
notes = [
str(note) for note in payload.get("notes", []) if isinstance(note, str)
]
has_missing = (
isinstance(missing_decisions, list) and len(missing_decisions) > 0
)
can_save_now = confidence == "high" and not has_missing
if can_save_now:
return cls(
can_continue=True,
can_save_now=True,
recommended_next_tool=NextActionTool.VALIDATE_DRAFT_WORKSPACE,
reason="Wrapper hints are high confidence and have no missing decisions.",
patch_examples=[],
warnings=[],
)
return cls(
can_continue=True,
can_save_now=False,
recommended_next_tool=NextActionTool.PATCH_DRAFT_WORKSPACE,
reason="Review missing wrapper decisions before saving.",
patch_examples=_wrapper_draft_patch_examples(
workspace_id=workspace_id,
revision=revision,
hints=payload,
),
warnings=notes,
)
def _wrapper_draft_patch_examples(
*,
workspace_id: str,
revision: int,
hints: dict[str, Any],
) -> list[NextActionPatchExample]:
"""Return conservative JSON Patch examples without guessing semantics."""
examples: list[NextActionPatchExample] = []
missing_decisions = hints.get("missing_decisions")
if not isinstance(missing_decisions, list):
return examples
decision_kinds = {
str(decision.get("kind"))
for decision in missing_decisions
if isinstance(decision, dict)
}
if {"choose_output_fields", "review_nested_output"} & decision_kinds:
examples.append(
NextActionPatchExample(
description=(
"Replace output bindings after choosing which capability "
"outputs should be written to workflow state."
),
tool=NextActionTool.PATCH_DRAFT_WORKSPACE,
request={
"workspace_id": workspace_id,
"revision": revision,
"patch": [
{
"op": "replace",
"path": "/draft/steps/call/output",
"value": [],
}
],
},
)
)
if "confirm_boolean_outcomes" in decision_kinds:
examples.append(
NextActionPatchExample(
description=(
"Review boolean output candidates before adding routing; "
"do not route on boolean fields automatically."
),
tool=NextActionTool.PATCH_DRAFT_WORKSPACE,
request={
"workspace_id": workspace_id,
"revision": revision,
"patch": [],
},
)
)
return examples
- Step 2: Run the focused unit test
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py -q
Expected: PASS.
Task 3: Replace Wrapper-Specific MCP Models with Generic Models
Files:
-
Modify:
src/wf_mcp/workflow_surface/models.py -
Step 1: Import generic next-action models
Near the other local imports in src/wf_mcp/workflow_surface/models.py, add:
from .next_actions import NextActionPatchExample, NextActions
- Step 2: Remove wrapper-specific model class bodies
Delete the current WrapperDraftPatchExample and WrapperDraftNextActions class definitions.
Replace them with compatibility aliases immediately before CreateDraftWorkspaceFromCapabilityResult:
# Compatibility aliases for older imports. The JSON fields are now generic
# workflow-surface guidance, not wrapper-only policy.
WrapperDraftPatchExample = NextActionPatchExample
WrapperDraftNextActions = NextActions
- Step 3: Keep
CreateDraftWorkspaceFromCapabilityResulttyped with the alias
Leave the field shape unchanged except that the alias now points at the generic type:
class CreateDraftWorkspaceFromCapabilityResult(DraftWorkspaceResult):
"""Draft workspace result plus wrapper hints and advisory next actions."""
wrapper_hints: dict[str, Any] = Field(
description=(
"The wrapper_hints payload used before applying request overrides. "
"Use this to patch uncertain maps or schemas by revision."
)
)
next_actions: WrapperDraftNextActions = Field(
description=(
"Advisory next step guidance derived from wrapper_hints. "
"The server does not enforce can_save_now."
)
)
- Step 4: Run model import check
Run:
uv run python -c "from wf_mcp.workflow_surface.models import WrapperDraftNextActions, CreateDraftWorkspaceFromCapabilityResult; print(WrapperDraftNextActions.__name__)"
Expected output includes:
NextActions
Task 4: Replace Handler Dict Helpers with NextActions
Files:
-
Modify:
src/wf_mcp/workflow_surface/handlers.py -
Step 1: Import
NextActions
Add to the local imports in src/wf_mcp/workflow_surface/handlers.py:
from .next_actions import NextActions
- Step 2: Update
create_draft_workspace_from_capability
Replace the next_actions part of the return value with:
return {
**result,
"wrapper_hints": hints,
"next_actions": NextActions.from_wrapper_hints(
workspace_id=workspace_id,
revision=int(result["revision"]),
hints=hints,
).model_dump(mode="json"),
}
- Step 3: Delete handler-local guidance helpers
Delete these functions from src/wf_mcp/workflow_surface/handlers.py:
def _wrapper_draft_next_actions(...)
def _wrapper_draft_patch_examples(...)
Do not delete _source_id_for_capability(...), which follows those helpers.
- Step 4: Run focused draft tests
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py tests/wf_mcp/workflow_surface/test_drafts.py -q
Expected: PASS except for assertions that still need additive can_continue checks in Task 5.
Task 5: Update Integration and Schema Tests
Files:
-
Modify:
tests/wf_mcp/workflow_surface/test_drafts.py -
Modify:
tests/wf_mcp/server/test_config.py -
Step 1: Add
can_continueassertions to draft integration tests
In tests/wf_mcp/workflow_surface/test_drafts.py, find the test that asserts high-confidence next_actions.
Add this assertion near the other next_actions assertions:
assert next_actions["can_continue"] is True
Keep these existing assertions:
assert next_actions["can_save_now"] is True
assert next_actions["recommended_next_tool"] == (
"wf.workflow.validate_draft_workspace"
)
assert "high confidence" in next_actions["reason"]
assert next_actions["patch_examples"] == []
assert next_actions["warnings"] == []
- Step 2: Add
can_continueassertion to low-confidence integration test
In the low-confidence/content-block test in tests/wf_mcp/workflow_surface/test_drafts.py, add:
assert next_actions["can_continue"] is True
Keep field-level assertions. Do not assert whole dict equality.
- Step 3: Update MCP schema test
In tests/wf_mcp/server/test_config.py, find the create_draft_workspace_from_capability output schema test.
Add:
next_actions_schema = result_schema["properties"]["next_actions"]
next_action_properties = next_actions_schema["properties"]
assert "can_continue" in next_action_properties
assert "Advisory" in next_action_properties["can_continue"]["description"]
assert "can_save_now" in next_action_properties
assert "Advisory" in next_action_properties["can_save_now"]["description"]
If the existing test already has next_actions_schema, reuse it. Do not assert the entire generated schema.
- Step 4: Run focused tests
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py -q
Expected: PASS.
Task 6: Add a Small Documentation Note
Files:
-
Modify:
docs/workflow_capabilities.md -
Step 1: Add an advisory-guidance note
Find the section that mentions next_actions or wrapper draft guidance. Add this paragraph:
`next_actions` is advisory guidance, not validation authority. It gives MCP
clients a compact "what should I call next?" pointer, while diagnostics,
artifact validation, deployment validation, and runtime status remain the
source of truth.
- Step 2: Verify docs reference still works
Run:
uv run pytest tests/wf_mcp/server/test_docs.py -q
Expected: PASS.
Task 7: Full Verification
Files:
-
All touched files.
-
Step 1: Run focused tests
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py tests/wf_mcp/server/test_docs.py -q
Expected: PASS.
- Step 2: Run formatting check on touched files
Run:
uv run ruff format --check src/wf_mcp/workflow_surface/next_actions.py src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/handlers.py tests/wf_mcp/workflow_surface/test_next_actions.py tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py
Expected: PASS.
- Step 3: Run lint on touched files
Run:
uv run ruff check src/wf_mcp/workflow_surface/next_actions.py src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/handlers.py tests/wf_mcp/workflow_surface/test_next_actions.py tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py
Expected: PASS.
- Step 4: Run type check
Run:
uv run basedpyright --level error
Expected: 0 errors.
- Step 5: Run the full test suite if time allows
Run:
uv run pytest -q
Expected: current suite status should remain unchanged from the baseline, currently about 723 passed, 1 skipped, 1 xfailed.
Self-Review Checklist
-
Spec coverage:
next_actions.pyis created.- Generic models exist.
- Wrapper-hint constructor exists.
- Handler-local dict helpers are removed.
- Existing JSON fields remain stable.
- Deployment/run guidance is intentionally deferred.
-
Placeholder scan:
- No
TBD. - No open-ended "add appropriate validation".
- Each code step includes exact snippets.
- No
-
Type consistency:
NextActionTool.PATCH_DRAFT_WORKSPACE.valueserializes to the existing string.WrapperDraftNextActionsremains importable as an alias.CreateDraftWorkspaceFromCapabilityResult.next_actionskeeps the same public response location.
Handoff Notes
This plan is intentionally small. It should not change workflow behavior or draft semantics. If any test fails outside next_actions schema/serialization, stop and inspect before broadening the change.