models + next action on deployment

This commit is contained in:
lda
2026-06-01 01:22:55 +07:00 Verified
parent 9d5748d8fc
commit ee1e98f410
9 changed files with 394 additions and 21 deletions
+10
View File
@@ -1001,6 +1001,10 @@ class WorkflowSurfaceHandlers:
"diagnostics": [
diagnostic.model_dump(mode="json") for diagnostic in diagnostics
],
"next_actions": NextActions.from_deployment_validation(
deployment_id=deployment.id,
diagnostics=diagnostics,
).model_dump(mode="json"),
}
async def run_deployment(
@@ -1584,6 +1588,12 @@ def _run_payload(
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"),
}
if trace is not None:
# Trace entries can grow quickly, so the public run tool only includes
+58
View File
@@ -377,6 +377,64 @@ class CreateDraftWorkspaceFromCapabilityResult(DraftWorkspaceResult):
)
class ValidateDeploymentResult(BaseModel):
"""Response contract for validate_deployment."""
deployment_id: str = Field(description="Deployment that was validated.")
artifact_id: str = Field(description="Artifact targeted by the deployment.")
artifact_version: int = Field(description="Artifact version.")
status: Literal["runnable", "unrunnable"] = Field(
description="Whether the deployment can be run."
)
diagnostics: list[dict[str, Any]] = Field(
default_factory=list,
description="Structured diagnostics for unrunnable deployments.",
)
next_actions: NextActions = Field(description="Advisory next step guidance.")
class RunDeploymentResult(BaseModel):
"""Response contract for run_deployment, inspect_run, resume_run, and read_run_trace."""
deployment_id: str = Field(description="Deployment that was run.")
artifact_id: str = Field(description="Artifact targeted by the deployment.")
artifact_version: int = Field(description="Artifact version.")
status: str = Field(description="Run status, such as completed, failed, or interrupted.")
run_id: str | None = Field(default=None, description="Durable run identifier.")
resume_readiness: str | None = Field(
default=None, description="Resume readiness state."
)
interrupt: dict[str, Any] | None = Field(
default=None, description="Interrupt payload if paused."
)
outcome: str | None = Field(default=None, description="Terminal outcome label.")
error: str | None = Field(default=None, description="Error message if failed.")
output: dict[str, Any] | None = Field(
default=None, description="Workflow output payload."
)
diagnostics: list[dict[str, Any]] = Field(
default_factory=list, description="Structured diagnostics."
)
trace_count: int = Field(default=0, description="Total trace entries.")
next_actions: NextActions = Field(description="Advisory next step guidance.")
trace_start: int | None = Field(
default=None,
description="First trace entry offset included when a trace slice was requested.",
)
trace_limit: int | None = Field(
default=None,
description="Maximum trace entries requested when a trace slice was included.",
)
trace: list[dict[str, Any]] | None = Field(
default=None,
description="Bounded debug trace slice, present only when explicitly requested.",
)
trace_truncated: bool = Field(
default=False,
description="Whether more trace entries exist after the returned slice.",
)
class CreateArtifactFromWorkspaceRequest(BaseModel):
"""Typed MCP request payload for saving a draft workspace as an artifact."""
+159
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Sequence
from enum import StrEnum
from typing import Any, Self
@@ -106,6 +107,122 @@ class NextActions(BaseModel):
warnings=notes,
)
@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,
)
@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":
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=[],
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,
)
def _wrapper_draft_patch_examples(
*,
@@ -160,3 +277,45 @@ def _wrapper_draft_patch_examples(
)
)
return examples
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": 25,
},
},
)
+33 -21
View File
@@ -23,11 +23,13 @@ from .models import (
DraftWorkspaceListResult,
DraftWorkspaceResult,
PatchDraftWorkspaceRequest,
RunDeploymentResult,
SetDraftNameRequest,
SetDraftRouteRequest,
SetStepInputMapRequest,
SetStepOutputMapRequest,
TraceRange,
ValidateDeploymentResult,
ValidateDraftWorkspaceRequest,
)
@@ -627,10 +629,12 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
)
),
] = False,
) -> dict[str, Any]:
return await handlers.validate_deployment(
deployment_id=deployment_id,
live_check=live_check,
) -> ValidateDeploymentResult:
return ValidateDeploymentResult.model_validate(
await handlers.validate_deployment(
deployment_id=deployment_id,
live_check=live_check,
)
)
@server.tool(
@@ -656,11 +660,13 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
)
),
] = None,
) -> dict[str, Any]:
return await handlers.run_deployment(
deployment_id=deployment_id,
workflow_input=workflow_input,
trace_range=trace_range,
) -> RunDeploymentResult:
return RunDeploymentResult.model_validate(
await handlers.run_deployment(
deployment_id=deployment_id,
workflow_input=workflow_input,
trace_range=trace_range,
)
)
@server.tool(
@@ -685,12 +691,14 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
)
),
] = None,
) -> dict[str, Any]:
return await handlers.resume_run(
run_id=run_id,
resume_payload=resume_payload,
resume_outcome=resume_outcome,
trace_range=trace_range,
) -> RunDeploymentResult:
return RunDeploymentResult.model_validate(
await handlers.resume_run(
run_id=run_id,
resume_payload=resume_payload,
resume_outcome=resume_outcome,
trace_range=trace_range,
)
)
@server.tool(
@@ -701,8 +709,10 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
"entries. Use read_run_trace only when trace detail is required."
),
)
async def inspect_run(run_id: str) -> dict[str, Any]:
return await handlers.inspect_run(run_id=run_id)
async def inspect_run(run_id: str) -> RunDeploymentResult:
return RunDeploymentResult.model_validate(
await handlers.inspect_run(run_id=run_id)
)
@server.tool(
name="wf.workflow.read_run_trace",
@@ -720,8 +730,10 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
)
),
],
) -> dict[str, Any]:
return await handlers.read_run_trace(
run_id=run_id,
trace_range=trace_range,
) -> RunDeploymentResult:
return RunDeploymentResult.model_validate(
await handlers.read_run_trace(
run_id=run_id,
trace_range=trace_range,
)
)