6.8 KiB
Workflow Surface Next Actions Design
Purpose
next_actions is becoming a reusable UX pattern for MCP-facing workflow tools.
It gives LLM clients a small, machine-readable answer to:
What should I call next?
This is especially useful when a client cannot easily read resources/prompts or when the MCP tool schema is technically correct but easy to misuse.
Boundary
next_actions is guidance, not authority.
- Diagnostics describe machine-readable facts about validity, drift, source liveness, blocked resume, and runtime failures.
next_actionsexplains the likely next useful tool call.- Runtime validation, artifact validation, and dependency validation remain the source of truth.
next_actions.can_save_nowis advisory only. It must not block saving.
Proposed Module
Create a focused module:
src/wf_mcp/workflow_surface/next_actions.py
It should own the reusable result models and constructors. This keeps
handlers.py from accumulating UX policy helpers and keeps models.py focused
on MCP request/response schemas.
Core Types
from __future__ import annotations
from enum import StrEnum
from typing import Any, Self
from pydantic import BaseModel, Field
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 the recommended tool."""
description: str
tool: NextActionTool
request: dict[str, Any]
class NextActions(BaseModel):
"""Advisory continuation hints for MCP workflow clients."""
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 it."
),
)
recommended_next_tool: NextActionTool | None = Field(
default=None,
description="Suggested next MCP workflow tool, if one is obvious.",
)
reason: str
patch_examples: list[NextActionPatchExample] = Field(default_factory=list)
warnings: list[str] = Field(default_factory=list)
Constructors
Prefer named constructors or classmethods over ad-hoc dict helpers.
Initial constructor:
@classmethod
def from_wrapper_hints(
cls,
*,
workspace_id: str,
revision: int,
hints: WrapperAuthoringHints | dict[str, Any],
) -> Self:
"""Create guidance after create_draft_workspace_from_capability."""
Later constructors:
@classmethod
def from_deployment_validation(
cls,
*,
deployment_id: str,
diagnostics: list[DependencyDiagnostic],
) -> Self:
"""Create guidance after validate_deployment."""
@classmethod
def from_run_result(
cls,
*,
run_id: str | None,
status: str,
trace_count: int,
diagnostics: list[DependencyDiagnostic],
) -> Self:
"""Create guidance after run_deployment, inspect_run, or resume_run."""
Wrapper Draft Guidance Rules
For create_draft_workspace_from_capability:
- High confidence and no missing decisions:
can_continue=truecan_save_now=truerecommended_next_tool=wf.workflow.validate_draft_workspace
- Low/medium confidence or any missing decisions:
can_continue=truecan_save_now=falserecommended_next_tool=wf.workflow.patch_draft_workspace- include conservative
patch_exampleswhen the missing decision can be represented safely
Patch examples must not invent business semantics.
Allowed examples:
- replace step output bindings with an empty list as a scaffold
- point the caller to
patch_draft_workspacewith the correctworkspace_idandrevision - include an empty patch for boolean-outcome decisions when no safe automatic route exists
Not allowed:
- auto-extract
content[0].text - auto-route on boolean output fields
- infer error outcome mapping from arbitrary output data
Future Deployment Guidance
For validate_deployment:
- No diagnostics:
- recommend
wf.workflow.run_deployment
- recommend
source_unreachable:- recommend fixing/reloading source, then
wf.workflow.validate_deploymentwithlive_check=true
- recommend fixing/reloading source, then
binding_missing:- recommend
wf.workflow.save_deployment
- recommend
capability_missingorschema_changed:- recommend inspecting capabilities or refreshing catalog before running
The exact deployment constructor can come later. The important rule is that
diagnostics stay the source of truth; next_actions only summarizes.
Future Run Guidance
For run_deployment, inspect_run, and resume_run:
status=completed:- no required next tool
- optionally suggest
read_run_traceonly if caller is debugging
status=failed:- recommend
inspect_runor boundedread_run_trace
- recommend
status=interrupted:- recommend
wf.workflow.resume_run
- recommend
- blocked resume:
- recommend repairing diagnostics and retrying
resume_run
- recommend repairing diagnostics and retrying
Never recommend reading the full trace. Always point to bounded trace ranges.
JSON Compatibility
Existing create_draft_workspace_from_capability output should remain stable:
{
"next_actions": {
"can_save_now": false,
"recommended_next_tool": "wf.workflow.patch_draft_workspace",
"reason": "...",
"patch_examples": [],
"warnings": []
}
}
If the generic model adds can_continue, it is additive. Existing clients that
only read can_save_now, recommended_next_tool, reason, patch_examples,
and warnings continue to work.
Migration Plan
- Add
next_actions.pywith generic models andfrom_wrapper_hints. - Re-export or import these models from
workflow_surface.modelsif needed for FastMCP schema generation. - Replace handler-local
_wrapper_draft_next_actionshelpers with the model constructor. - Keep response JSON fields stable.
- Add tests proving the old
next_actionsfields still serialize the same. - Later, add deployment/run constructors in separate small passes.
Open Questions
- Should
recommended_next_toolallow non-workflow tools such aswf.admin.reload_config, or should guidance stay workflow-surface only? - Should patch examples grow a typed
tool_request_schemalater, or is raw JSON request payload enough? - Should
can_continue=falseappear when there is no useful next tool, or shouldrecommended_next_tool=nullbe enough?