23 KiB
Deployment And Run Next Actions 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: Add advisory next_actions guidance to deployment validation and run lifecycle responses so MCP clients know the safest next workflow tool to call.
Architecture: Extend the existing src/wf_mcp/workflow_surface/next_actions.py module with deployment/run constructors instead of putting more UX policy in handlers.py. Thread the resulting NextActions object into validate_deployment(...) and _run_payload(...), which covers run_deployment, resume_run, inspect_run, and read_run_trace. Keep diagnostics and runtime status authoritative; next_actions remains advisory.
Tech Stack: Python 3.14, Pydantic v2, pytest, ruff, basedpyright.
File Structure
-
Modify
src/wf_mcp/workflow_surface/next_actions.py- Add
NextActions.from_deployment_validation(...). - Add
NextActions.from_run_result(...). - Add tiny private helpers for diagnostic code inspection and bounded trace examples.
- Add
-
Modify
src/wf_mcp/workflow_surface/handlers.py- Add
next_actionstovalidate_deployment(...). - Add optional
next_actionsconstruction inside_run_payload(...). - Do not duplicate branching logic in individual handler methods.
- Add
-
Modify
tests/wf_mcp/workflow_surface/test_next_actions.py- Unit-test deployment/run constructors directly.
-
Modify
tests/wf_mcp/workflow_surface/test_deployments.py- Assert
validate_deploymentreturns useful next actions for runnable and unrunnable deployments.
- Assert
-
Modify
tests/wf_mcp/workflow_surface/test_runs.py- Assert run lifecycle responses return next actions for completed and failed runs.
- Add interrupt/resume tests only if an existing fixture/helper makes this small; otherwise leave interrupt-specific coverage to constructor unit tests.
-
Modify
tests/wf_mcp/server/test_config.py- Assert output schemas for
validate_deploymentandrun_deploymentincludenext_actions.
- Assert output schemas for
-
Modify
docs/workflow_capabilities.md- Add a short note that deployment/run responses now include advisory
next_actions.
- Add a short note that deployment/run responses now include advisory
Scope Boundaries
- Do not create new workflow tools.
- Do not make
next_actionsrequired for correctness. - Do not read or return full traces automatically.
- Do not add auto-repair behavior.
- Do not change existing status strings, diagnostics, or run payload fields.
- Do not implement persisted resume changes in this pass.
Task 1: Add Constructor Unit Tests
Files:
-
Modify:
tests/wf_mcp/workflow_surface/test_next_actions.py -
Step 1: Add imports
At the top of tests/wf_mcp/workflow_surface/test_next_actions.py, add:
from wf_artifacts import DependencyDiagnostic, DiagnosticSeverity
Keep the existing NextActionTool / NextActions import.
- Step 2: Add deployment validation constructor tests
Append to tests/wf_mcp/workflow_surface/test_next_actions.py:
def test_next_actions_from_runnable_deployment_recommends_run() -> None:
actions = NextActions.from_deployment_validation(
deployment_id="echo.personal",
diagnostics=[],
)
dumped = actions.model_dump(mode="json")
assert dumped["can_continue"] is True
assert dumped["recommended_next_tool"] == NextActionTool.RUN_DEPLOYMENT.value
assert "run_deployment" in dumped["reason"]
assert dumped["warnings"] == []
def test_next_actions_from_unrunnable_deployment_recommends_validation_retry() -> None:
diagnostic = DependencyDiagnostic(
severity=DiagnosticSeverity.ERROR,
code="source_unreachable",
logical_ref="demo.echo_tool",
bound_source="demo.personal",
message="Live check for upstream source 'demo.personal' failed.",
repair_hint="Start or reconnect the source.",
)
actions = NextActions.from_deployment_validation(
deployment_id="echo.personal",
diagnostics=[diagnostic],
)
dumped = actions.model_dump(mode="json")
assert dumped["can_continue"] is True
assert dumped["recommended_next_tool"] == NextActionTool.VALIDATE_DEPLOYMENT.value
assert "fix or reconnect" in dumped["reason"]
assert dumped["warnings"][0] == "source_unreachable: demo.personal"
- Step 3: Add run result constructor tests
Append to tests/wf_mcp/workflow_surface/test_next_actions.py:
def test_next_actions_from_completed_run_has_no_required_next_tool() -> None:
actions = NextActions.from_run_result(
run_id="run_123",
status="completed",
trace_count=2,
diagnostics=[],
)
dumped = actions.model_dump(mode="json")
assert dumped["can_continue"] is False
assert dumped["recommended_next_tool"] is None
assert "completed" in dumped["reason"]
assert dumped["patch_examples"] == []
def test_next_actions_from_failed_run_recommends_bounded_trace() -> None:
actions = NextActions.from_run_result(
run_id="run_123",
status="failed",
trace_count=12,
diagnostics=[],
)
dumped = actions.model_dump(mode="json")
assert dumped["can_continue"] is True
assert dumped["recommended_next_tool"] == NextActionTool.READ_RUN_TRACE.value
assert "bounded trace" in dumped["reason"]
assert dumped["patch_examples"][0]["tool"] == NextActionTool.READ_RUN_TRACE.value
assert dumped["patch_examples"][0]["request"]["run_id"] == "run_123"
assert dumped["patch_examples"][0]["request"]["trace_range"]["start"] == 0
assert dumped["patch_examples"][0]["request"]["trace_range"]["limit"] == 25
def test_next_actions_from_interrupted_run_recommends_resume() -> None:
actions = NextActions.from_run_result(
run_id="run_123",
status="interrupted",
trace_count=3,
diagnostics=[],
)
dumped = actions.model_dump(mode="json")
assert dumped["can_continue"] is True
assert dumped["recommended_next_tool"] == NextActionTool.RESUME_RUN.value
assert "resume_run" in dumped["reason"]
assert dumped["patch_examples"] == []
- Step 4: Run the new tests to verify they fail
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py -q
Expected: FAIL with AttributeError for missing from_deployment_validation and from_run_result.
Task 2: Implement Deployment/Run Constructors
Files:
-
Modify:
src/wf_mcp/workflow_surface/next_actions.py -
Step 1: Add type-only imports
In src/wf_mcp/workflow_surface/next_actions.py, add:
from collections.abc import Sequence
Do not import DependencyDiagnostic directly unless needed at runtime. The constructors can accept diagnostics as objects or dicts to keep coupling low.
- Step 2: Add
from_deployment_validation
Inside class NextActions, after from_wrapper_hints(...), add:
@classmethod
def from_deployment_validation(
cls,
*,
deployment_id: str,
diagnostics: Sequence[object],
) -> Self:
"""Create guidance after validate_deployment."""
if not diagnostics:
return cls(
can_continue=True,
can_save_now=None,
recommended_next_tool=NextActionTool.RUN_DEPLOYMENT,
reason=(
f"Deployment {deployment_id!r} is runnable; call "
"wf.workflow.run_deployment with workflow_input."
),
patch_examples=[],
warnings=[],
)
codes = {_diagnostic_field(diagnostic, "code") for diagnostic in diagnostics}
warnings = [_diagnostic_warning(diagnostic) for diagnostic in diagnostics]
if "source_unreachable" in codes:
reason = (
"One or more live sources are unreachable; fix or reconnect the "
"source, then rerun wf.workflow.validate_deployment with live_check=true."
)
elif "source_missing" in codes or "binding_missing" in codes:
reason = (
"Deployment bindings or sources are missing; inspect the deployment "
"and save corrected bindings before running."
)
elif "capability_missing" in codes or "schema_changed" in codes:
reason = (
"A required capability is missing or drifted; inspect capabilities "
"or refresh sources, then validate again."
)
else:
reason = (
"Deployment is not runnable; inspect diagnostics, repair the "
"deployment or sources, then validate again."
)
return cls(
can_continue=True,
can_save_now=None,
recommended_next_tool=NextActionTool.VALIDATE_DEPLOYMENT,
reason=reason,
patch_examples=[],
warnings=warnings,
)
- Step 3: Add
from_run_result
Inside class NextActions, after from_deployment_validation(...), add:
@classmethod
def from_run_result(
cls,
*,
run_id: str | None,
status: str,
trace_count: int,
diagnostics: Sequence[object],
) -> Self:
"""Create guidance after run_deployment, inspect_run, resume_run, or read_run_trace."""
warnings = [_diagnostic_warning(diagnostic) for diagnostic in diagnostics]
if status == "interrupted" and run_id is not None:
return cls(
can_continue=True,
can_save_now=None,
recommended_next_tool=NextActionTool.RESUME_RUN,
reason=(
"Run is interrupted; call wf.workflow.resume_run with this "
"run_id and the interrupt response payload."
),
patch_examples=[],
warnings=warnings,
)
if status in {"failed", "unrunnable"}:
examples = (
[_bounded_trace_example(run_id=run_id, trace_count=trace_count)]
if run_id is not None and trace_count > 0
else []
)
return cls(
can_continue=bool(examples),
can_save_now=None,
recommended_next_tool=(
NextActionTool.READ_RUN_TRACE if examples else None
),
reason=(
"Run failed; read a bounded trace slice for debugging."
if examples
else "Run failed before producing trace entries; inspect diagnostics and error."
),
patch_examples=examples,
warnings=warnings,
)
if status == "completed":
examples = (
[_bounded_trace_example(run_id=run_id, trace_count=trace_count)]
if run_id is not None and trace_count > 0
else []
)
return cls(
can_continue=False,
can_save_now=None,
recommended_next_tool=None,
reason=(
"Run completed. No required next workflow tool; use read_run_trace "
"with a bounded trace_range only if debugging."
),
patch_examples=examples,
warnings=warnings,
)
return cls(
can_continue=False,
can_save_now=None,
recommended_next_tool=None,
reason=f"Run status {status!r} has no obvious next workflow tool.",
patch_examples=[],
warnings=warnings,
)
- Step 4: Add private diagnostic helpers
Below _wrapper_draft_patch_examples(...), add:
def _diagnostic_field(diagnostic: object, field: str) -> str | None:
"""Read a diagnostic field from either a Pydantic model or a JSON dict."""
if isinstance(diagnostic, dict):
value = diagnostic.get(field)
else:
value = getattr(diagnostic, field, None)
return value if isinstance(value, str) else None
def _diagnostic_warning(diagnostic: object) -> str:
"""Format one compact diagnostic warning for next_actions."""
code = _diagnostic_field(diagnostic, "code") or "diagnostic"
bound_source = _diagnostic_field(diagnostic, "bound_source")
logical_ref = _diagnostic_field(diagnostic, "logical_ref")
if bound_source:
return f"{code}: {bound_source}"
if logical_ref:
return f"{code}: {logical_ref}"
return code
def _bounded_trace_example(
*,
run_id: str,
trace_count: int,
) -> NextActionPatchExample:
"""Return a safe read_run_trace request; never suggest full trace reads."""
return NextActionPatchExample(
description=(
"Read a bounded debug trace slice. Increase start/limit only when needed."
),
tool=NextActionTool.READ_RUN_TRACE,
request={
"run_id": run_id,
"trace_range": {
"start": 0,
"limit": min(trace_count, 25),
},
},
)
- Step 5: Run constructor tests
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py -q
Expected: PASS.
Task 3: Thread NextActions Through Deployment Validation
Files:
-
Modify:
src/wf_mcp/workflow_surface/handlers.py -
Modify:
tests/wf_mcp/workflow_surface/test_deployments.py -
Step 1: Add deployment assertions
In test_workflow_surface_validate_deployment_live_check_is_opt_in, after current payload assertions, add:
assert payload["next_actions"]["can_continue"] is True
assert payload["next_actions"]["recommended_next_tool"] == (
"wf.workflow.run_deployment"
)
In test_workflow_surface_validates_deployment_dependencies, after diagnostic assertions, add:
assert payload["next_actions"]["can_continue"] is True
assert payload["next_actions"]["recommended_next_tool"] == (
"wf.workflow.validate_deployment"
)
assert payload["next_actions"]["warnings"][0] == "source_missing: context7.personal"
If the exact warning uses context7 instead of context7.personal, keep the assertion stable by checking fields separately:
assert payload["next_actions"]["warnings"]
assert "source_missing" in payload["next_actions"]["warnings"][0]
Prefer the exact assertion only if the implementation returns bound_source.
- Step 2: Update
validate_deploymentreturn payload
In src/wf_mcp/workflow_surface/handlers.py, replace the return body in validate_deployment(...) with:
status = "unrunnable" if diagnostics else "runnable"
diagnostic_payloads = [
diagnostic.model_dump(mode="json") for diagnostic in diagnostics
]
return {
"deployment_id": deployment.id,
"artifact_id": artifact.id,
"artifact_version": artifact.version,
"status": status,
"diagnostics": diagnostic_payloads,
"next_actions": NextActions.from_deployment_validation(
deployment_id=deployment.id,
diagnostics=diagnostics,
).model_dump(mode="json"),
}
- Step 3: Run deployment tests
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_deployments.py tests/wf_mcp/workflow_surface/test_next_actions.py -q
Expected: PASS.
Task 4: Thread NextActions Through Run Payloads
Files:
-
Modify:
src/wf_mcp/workflow_surface/handlers.py -
Modify:
tests/wf_mcp/workflow_surface/test_runs.py -
Step 1: Add completed-run assertions
In test_workflow_surface_runs_non_interrupting_deployment, after current run payload assertions, add:
assert payload["next_actions"]["can_continue"] is False
assert payload["next_actions"]["recommended_next_tool"] is None
assert "completed" in payload["next_actions"]["reason"]
After the inspected assertions in the same test, add:
assert inspected["next_actions"]["can_continue"] is False
assert inspected["next_actions"]["recommended_next_tool"] is None
- Step 2: Add failed-run assertions
In test_workflow_surface_failed_deployment_exposes_error_on_run_and_inspect, after current assertions, add:
assert payload["next_actions"]["recommended_next_tool"] is None
assert "before producing trace" in payload["next_actions"]["reason"]
assert inspected["next_actions"]["recommended_next_tool"] is None
This failing artifact currently has trace_count == 0, so the safe guidance is diagnostics/error, not read_run_trace.
- Step 3: Add trace-detail assertion
In test_workflow_surface_run_deployment_can_include_trace_detail, after the current trace assertions, add:
assert payload["next_actions"]["patch_examples"][0]["request"]["trace_range"][
"limit"
] == 1
This confirms completed runs may include bounded trace guidance without making it required.
- Step 4: Update
_run_payload
In src/wf_mcp/workflow_surface/handlers.py, inside _run_payload(...), add next_actions to the base payload dict:
"next_actions": NextActions.from_run_result(
run_id=run_id,
status=status,
trace_count=trace_count,
diagnostics=diagnostics or [],
).model_dump(mode="json"),
The resulting base payload should include:
payload = {
"deployment_id": deployment.id,
"artifact_id": artifact.id,
"artifact_version": artifact.version,
"status": status,
"run_id": run_id,
"resume_readiness": resume_readiness,
"interrupt": interrupt,
"outcome": outcome,
"error": error,
"output": output,
"diagnostics": [
diagnostic.model_dump(mode="json") for diagnostic in diagnostics or []
],
"trace_count": trace_count,
"next_actions": NextActions.from_run_result(
run_id=run_id,
status=status,
trace_count=trace_count,
diagnostics=diagnostics or [],
).model_dump(mode="json"),
}
- Step 5: Run run tests
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_runs.py tests/wf_mcp/workflow_surface/test_next_actions.py -q
Expected: PASS.
Task 5: Update MCP Output Schema Tests
Files:
-
Modify:
tests/wf_mcp/server/test_config.py -
Modify:
tests/wf_mcp/server/test_tools.py -
Step 1: Add schema assertions for deployment/run outputs
In tests/wf_mcp/server/test_config.py, find the existing workflow-surface schema test that checks create_draft_workspace_from_capability output contains next_actions.
Add assertions for wf.workflow.validate_deployment and wf.workflow.run_deployment output schemas. Use the same local by_name / tool lookup style already in that test file:
validate_deployment = by_name["wf.workflow.validate_deployment"]
run_deployment = by_name["wf.workflow.run_deployment"]
validate_output = validate_deployment.outputSchema
run_output = run_deployment.outputSchema
assert "next_actions" in validate_output["properties"]
assert "recommended_next_tool" in validate_output["properties"]["next_actions"][
"properties"
]
assert "next_actions" in run_output["properties"]
assert "recommended_next_tool" in run_output["properties"]["next_actions"][
"properties"
]
If this test file uses tool.outputSchema through dict access instead of attributes, follow the existing style in the file. Do not assert the whole schema.
- Step 2: Add tool description assertion only if output schema exists
In tests/wf_mcp/server/test_tools.py, add a small assertion that run_deployment.description or the output schema describes next_actions only if that file already inspects output schemas. Do not add fragile full-schema assertions.
If test_tools.py only checks input schemas and titles, skip this step.
- Step 3: Run server tests
Run:
uv run pytest tests/wf_mcp/server/test_config.py tests/wf_mcp/server/test_tools.py -q
Expected: PASS.
Task 6: Update Docs
Files:
-
Modify:
docs/workflow_capabilities.md -
Step 1: Add deployment/run guidance note
Add this paragraph near the existing next_actions explanation:
Deployment validation and run lifecycle responses also expose `next_actions`.
For runnable deployments this points to `wf.workflow.run_deployment`; for
unrunnable deployments it points back to validation after the caller repairs
bindings, sources, or schema drift. Failed runs never suggest reading an
unbounded trace; trace guidance always uses a bounded `trace_range`.
- Step 2: Run docs tests
Run:
uv run pytest tests/wf_mcp/server/test_docs.py -q
Expected: PASS.
Task 7: Verification
Files:
-
All touched files.
-
Step 1: Run focused test set
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_next_actions.py tests/wf_mcp/workflow_surface/test_deployments.py tests/wf_mcp/workflow_surface/test_runs.py tests/wf_mcp/server/test_config.py tests/wf_mcp/server/test_tools.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/handlers.py tests/wf_mcp/workflow_surface/test_next_actions.py tests/wf_mcp/workflow_surface/test_deployments.py tests/wf_mcp/workflow_surface/test_runs.py tests/wf_mcp/server/test_config.py tests/wf_mcp/server/test_tools.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/handlers.py tests/wf_mcp/workflow_surface/test_next_actions.py tests/wf_mcp/workflow_surface/test_deployments.py tests/wf_mcp/workflow_surface/test_runs.py tests/wf_mcp/server/test_config.py tests/wf_mcp/server/test_tools.py
Expected: PASS.
- Step 4: Run type check
Run:
uv run basedpyright --level error
Expected: 0 errors.
- Step 5: Optional full suite
Run:
uv run pytest -q
Expected: full suite remains green with the existing skip/xfail count.
Self-Review Checklist
next_actionsis present onvalidate_deploymentresponses.next_actionsis present on all_run_payload(...)responses.- Completed runs do not require another tool.
- Failed runs recommend bounded trace only when a trace exists.
- Interrupted runs recommend
wf.workflow.resume_run. - Diagnostics remain authoritative; guidance only summarizes.
- No unbounded trace read is suggested.
- Existing fields remain unchanged.
Notes For Opencode
- Keep this as an additive UX change.
- Do not make any runtime/deployment behavior depend on
next_actions. - Do not add a new tool.
- Do not broaden trace payloads.
- If server schema tests are awkward, prefer field-level assertions over whole-schema equality.