feat: type draft workspace results

This commit is contained in:
lda
2026-07-30 22:59:27 +07:00 Verified
parent eae6367d99
commit e60c391610
11 changed files with 520 additions and 235 deletions
+7 -6
View File
@@ -71,12 +71,13 @@
- A 2026-07-30 spike confirmed that `fastapi-jsonrpc` already exports a
complete OpenRPC document for all 70 registered methods. Request payloads
retain useful Pydantic schemas, so OpenRPC is a viable transport input.
- Typed-result slices now give `workflow.health` and all artifact, deployment,
and run operations named transport-neutral result schemas: 16 of 70
methods. The remaining 54 success results still collapse to generic objects
because their Python API and JSON-RPC handlers return `dict[str, Any]`.
Continue introducing operation result DTOs before adopting generated
TypeScript contracts.
- Typed-result slices now give `workflow.health`, all artifact, deployment,
and run operations, and 26 uniform persisted draft-workspace operations
named transport-neutral result schemas: 42 of 70 methods. The remaining 28
success results still collapse to generic objects, including exceptional
draft compile/save shapes and capability/source/admin operations. Continue
introducing operation result DTOs before adopting generated TypeScript
contracts.
- The stock `@open-rpc/generator` TypeScript client is not suitable here. It
exhausted a 4 GB Node heap on the full contract and emitted invalid dotted
class members plus `any` results for a minimal `workflow.health` contract.
+27 -24
View File
@@ -58,6 +58,7 @@ from .drafts import (
_draft_input_maps,
_draft_output_map,
)
from .models import DraftWorkspaceResult, JsonProjector
from .operation_context import WorkflowOperationContext
from .schema_projection import (
project_output_property_to_state_schema,
@@ -67,6 +68,8 @@ from .schema_projection import (
validate_json_value_at_schema_path,
)
_PROJECT_DRAFT_WORKSPACE = JsonProjector(DraftWorkspaceResult)
def _graph_parts(path: str) -> tuple[str, tuple[str, ...]]:
parsed = GraphSourcePath.parse(path)
@@ -322,7 +325,7 @@ class WorkflowDraftAuthoringApi:
*,
workspace_id: str,
revision: int,
) -> WorkflowDraftWorkspace | dict[str, Any]:
) -> WorkflowDraftWorkspace | DraftWorkspaceResult:
"""Load a workspace and enforce optimistic locking before semantic preflight."""
return self.drafts._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -367,7 +370,7 @@ class WorkflowDraftAuthoringApi:
step: DraftStep,
incoming: RouteSource | None = None,
routes: dict[str, str] | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Add one typed draft step and optional route edits in one revision."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -471,7 +474,7 @@ class WorkflowDraftAuthoringApi:
output_map: dict[str, str] | None = None,
error_message_source: str | GraphSourcePath | None = None,
title: str | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Bootstrap the smallest patchable draft around one workflow capability."""
draft_input, draft_with = _draft_input_maps(
input=input,
@@ -530,7 +533,7 @@ class WorkflowDraftAuthoringApi:
revision: int,
step_id: str,
bindings: Sequence[InputBinding],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Replace one capability step's canonical input bindings atomically."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -556,7 +559,7 @@ class WorkflowDraftAuthoringApi:
and workspace.draft.get("input_schema", {}) == projected.input_schema
and workspace.draft.get("state_schema", {}) == projected.state_schema
):
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
patch = _step_input_bindings_patch(
workspace=workspace,
@@ -651,7 +654,7 @@ class WorkflowDraftAuthoringApi:
revision: int,
step_id: str,
update: CapabilityStepUpdate,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Return a workspace summary or conflict after one atomic step patch."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -718,7 +721,7 @@ class WorkflowDraftAuthoringApi:
and workspace.draft.get("input_schema", {}) == input_schema
and workspace.draft.get("state_schema", {}) == state_schema
):
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
next_draft = deepcopy(workspace.draft)
next_steps = next_draft.get("steps")
@@ -739,7 +742,7 @@ class WorkflowDraftAuthoringApi:
workspace_id: str,
revision: int,
bindings: Sequence[InputBinding],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Replace canonical workflow output bindings atomically."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -839,7 +842,7 @@ class WorkflowDraftAuthoringApi:
payload = [binding.model_dump(mode="json") for binding in bindings]
if workspace.draft.get("output", []) == payload and projected == output_schema:
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
patch: list[dict[str, Any]] = []
if projected != output_schema:
@@ -864,7 +867,7 @@ class WorkflowDraftAuthoringApi:
revision: int,
step_id: str,
bindings: Sequence[OutputBinding],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Replace one capability step's canonical output bindings atomically."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -924,7 +927,7 @@ class WorkflowDraftAuthoringApi:
step.get("output", []) == payload
and workspace.draft.get("state_schema", {}) == projected_state
):
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
return await self.drafts.patch_draft_workspace(
workspace_id=workspace_id,
@@ -945,7 +948,7 @@ class WorkflowDraftAuthoringApi:
step_id: str,
source_path: str,
target_path: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Bind a graph path to or from one capability-local path."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -1141,7 +1144,7 @@ class WorkflowDraftAuthoringApi:
desc: str | None = None,
retry: int | None = None,
timeout_seconds: int | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Add one capability step plus explicit route/map/schema wiring.
This is a composed authoring helper for agents. It edits the draft in
@@ -1321,7 +1324,7 @@ class WorkflowDraftAuthoringApi:
revision: int,
step_id: str,
routes: dict[str, str],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Atomically set routes for one step, preserving unspecified outcomes."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -1338,7 +1341,7 @@ class WorkflowDraftAuthoringApi:
raise ValueError(f"routes for step {step_id!r} must be an object")
merged = {**existing, **routes}
if merged == existing:
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
return await self.drafts.patch_draft_workspace(
workspace_id=workspace_id,
revision=revision,
@@ -1358,7 +1361,7 @@ class WorkflowDraftAuthoringApi:
revision: int,
branches: Sequence[RouteSource],
target: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Update the target for multiple (step, outcome) pairs atomically."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -1368,7 +1371,7 @@ class WorkflowDraftAuthoringApi:
return checked
workspace = checked
if not branches:
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
draft_routes = workspace.draft.get("routes", {})
if not isinstance(draft_routes, dict):
raise ValueError("draft routes must be an object")
@@ -1397,7 +1400,7 @@ class WorkflowDraftAuthoringApi:
}
)
if not patch:
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
return await self.drafts.patch_draft_workspace(
workspace_id=workspace_id,
revision=revision,
@@ -1411,7 +1414,7 @@ class WorkflowDraftAuthoringApi:
revision: int,
step_id: str,
outcome: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Remove one route; missing routes are revision-checked no-ops."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -1427,7 +1430,7 @@ class WorkflowDraftAuthoringApi:
if not isinstance(step_routes, dict):
raise ValueError(f"routes for step {step_id!r} must be an object")
if outcome not in step_routes:
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
return await self.drafts.patch_draft_workspace(
workspace_id=workspace_id,
revision=revision,
@@ -1448,7 +1451,7 @@ class WorkflowDraftAuthoringApi:
workspace_id: str,
revision: int,
step_id: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Remove a step and its own route map; inbound routes are left explicit."""
checked = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -1461,7 +1464,7 @@ class WorkflowDraftAuthoringApi:
if not isinstance(steps, dict):
raise ValueError("draft steps must be an object")
if step_id not in steps:
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
patch = [
{
"op": "remove",
@@ -1490,7 +1493,7 @@ class WorkflowDraftAuthoringApi:
step_id: str,
inputs: Sequence[str] = (),
outputs: Sequence[str] = (),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Remove selected local input/output bindings from one draft step."""
if not inputs and not outputs:
raise ValueError("pass at least one input or output binding to remove")
@@ -1525,7 +1528,7 @@ class WorkflowDraftAuthoringApi:
item for item in current_outputs if item.get("source") not in output_sources
]
if next_inputs == current_inputs and next_outputs == current_outputs:
return summarize_draft_workspace(workspace)
return _PROJECT_DRAFT_WORKSPACE(summarize_draft_workspace(workspace))
patch: list[dict[str, Any]] = []
if next_inputs != current_inputs:
patch.append(
+101 -69
View File
@@ -52,9 +52,19 @@ from .draft_payloads import (
from .draft_payloads import (
output_bindings_payload as _draft_output_bindings_payload,
)
from .models import (
DeleteDraftWorkspaceResult,
DraftWorkspaceResult,
JsonProjector,
ListDraftWorkspacesResult,
)
from .operation_context import WorkflowOperationContext
from .schema_projection import project_property_to_schema_path, schema_path_exists
_PROJECT_DRAFT_WORKSPACE = JsonProjector(DraftWorkspaceResult)
_PROJECT_DRAFT_WORKSPACE_LIST = JsonProjector(ListDraftWorkspacesResult)
_PROJECT_DRAFT_WORKSPACE_DELETE = JsonProjector(DeleteDraftWorkspaceResult)
def _empty_object_schema() -> dict[str, Any]:
"""Return one fresh unconstrained object schema for an empty draft."""
@@ -108,25 +118,27 @@ class WorkflowDraftApi:
*,
workspace_id: str,
revision: int,
) -> WorkflowDraftWorkspace | dict[str, Any]:
) -> WorkflowDraftWorkspace | DraftWorkspaceResult:
"""Load a workspace or return its canonical revision-conflict payload."""
workspace = self._draft_store().get_workspace(workspace_id)
if workspace.revision == revision:
return workspace
return {
**summarize_draft_workspace(workspace),
"status": "conflict",
"diagnostics": [
{
"code": "revision_conflict",
"path": "revision",
"message": (
f"workspace {workspace.id!r} is at revision "
f"{workspace.revision}, not {revision}"
),
}
],
}
return _PROJECT_DRAFT_WORKSPACE(
{
**summarize_draft_workspace(workspace),
"status": "conflict",
"diagnostics": [
{
"code": "revision_conflict",
"path": "revision",
"message": (
f"workspace {workspace.id!r} is at revision "
f"{workspace.revision}, not {revision}"
),
}
],
}
)
def _outcomes_for_capability(self, qualified_name: str) -> tuple[str, ...] | None:
try:
@@ -189,15 +201,17 @@ class WorkflowDraftApi:
node_defs_for_draft=self._node_defs_for_draft,
)
async def list_draft_workspaces(self) -> dict[str, Any]:
async def list_draft_workspaces(self) -> ListDraftWorkspacesResult:
"""Return compact summaries for stored draft workspaces."""
store = self._draft_store()
return {
"workspaces": [
get_draft_workspace_record(store, workspace_id=workspace.id)
for workspace in store.list_workspaces()
]
}
return _PROJECT_DRAFT_WORKSPACE_LIST(
{
"workspaces": [
get_draft_workspace_record(store, workspace_id=workspace.id)
for workspace in store.list_workspaces()
]
}
)
async def create_draft_workspace(
self,
@@ -205,12 +219,14 @@ class WorkflowDraftApi:
workspace_id: str,
draft: dict[str, Any],
title: str | None = None,
) -> dict[str, Any]:
return create_draft_workspace_record(
self._draft_store(),
workspace_id=workspace_id,
draft=draft,
title=title,
) -> DraftWorkspaceResult:
return _PROJECT_DRAFT_WORKSPACE(
create_draft_workspace_record(
self._draft_store(),
workspace_id=workspace_id,
draft=draft,
title=title,
)
)
async def create_empty_draft_workspace(
@@ -223,7 +239,7 @@ class WorkflowDraftApi:
state_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None,
outcomes: Sequence[str] = ("ok",),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Create an intentionally invalid, capability-free draft workspace.
The empty entry point is persisted so callers can assemble the graph in
@@ -272,22 +288,30 @@ class WorkflowDraftApi:
*,
workspace_id: str,
include_draft: bool = False,
) -> dict[str, Any]:
return get_draft_workspace_record(
self._draft_store(),
workspace_id=workspace_id,
include_draft=include_draft,
) -> DraftWorkspaceResult:
return _PROJECT_DRAFT_WORKSPACE(
get_draft_workspace_record(
self._draft_store(),
workspace_id=workspace_id,
include_draft=include_draft,
)
)
async def delete_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
async def delete_draft_workspace(
self, *, workspace_id: str
) -> DeleteDraftWorkspaceResult:
deleted = self._draft_store().delete_workspace(workspace_id)
return {
"workspace_id": workspace_id,
"deleted": deleted,
"status": "deleted" if deleted else "not_found",
}
return _PROJECT_DRAFT_WORKSPACE_DELETE(
{
"workspace_id": workspace_id,
"deleted": deleted,
"status": "deleted" if deleted else "not_found",
}
)
async def validate_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
async def validate_draft_workspace(
self, *, workspace_id: str
) -> DraftWorkspaceResult:
"""Refresh stored validation status without changing draft revision."""
store = self._draft_store()
workspace = store.get_workspace(workspace_id)
@@ -303,7 +327,9 @@ class WorkflowDraftApi:
}
)
store.save_workspace(refreshed)
return get_draft_workspace_record(store, workspace_id=workspace_id)
return _PROJECT_DRAFT_WORKSPACE(
get_draft_workspace_record(store, workspace_id=workspace_id)
)
async def compile_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
"""Compile a stored draft workspace without mutating it."""
@@ -319,14 +345,16 @@ class WorkflowDraftApi:
workspace_id: str,
revision: int,
patch: list[dict[str, Any]],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
store = self._draft_store()
return patch_draft_workspace_record(
store,
workspace_id=workspace_id,
revision=revision,
patch=patch,
node_defs_for_draft=self._node_defs_for_draft,
return _PROJECT_DRAFT_WORKSPACE(
patch_draft_workspace_record(
store,
workspace_id=workspace_id,
revision=revision,
patch=patch,
node_defs_for_draft=self._node_defs_for_draft,
)
)
async def replace_validated_draft_document(
@@ -335,13 +363,15 @@ class WorkflowDraftApi:
workspace_id: str,
revision: int,
draft: dict[str, Any],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Persist a focused, structurally validated edit without provider lookup."""
return replace_validated_draft_document(
self._draft_store(),
workspace_id=workspace_id,
revision=revision,
draft=draft,
return _PROJECT_DRAFT_WORKSPACE(
replace_validated_draft_document(
self._draft_store(),
workspace_id=workspace_id,
revision=revision,
draft=draft,
)
)
async def replace_draft_workspace_document(
@@ -350,14 +380,16 @@ class WorkflowDraftApi:
workspace_id: str,
revision: int,
draft: dict[str, Any],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Replace and semantically revalidate one complete workspace draft."""
return replace_draft_workspace_document_record(
self._draft_store(),
workspace_id=workspace_id,
revision=revision,
draft=draft,
node_defs_for_draft=self._node_defs_for_draft,
return _PROJECT_DRAFT_WORKSPACE(
replace_draft_workspace_document_record(
self._draft_store(),
workspace_id=workspace_id,
revision=revision,
draft=draft,
node_defs_for_draft=self._node_defs_for_draft,
)
)
async def set_draft_name(
@@ -366,7 +398,7 @@ class WorkflowDraftApi:
workspace_id: str,
revision: int,
name: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.patch_draft_workspace(
workspace_id=workspace_id,
revision=revision,
@@ -379,7 +411,7 @@ class WorkflowDraftApi:
workspace_id: str,
revision: int,
step_id: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Select an entry point, including a forward-referenced step id."""
if not isinstance(step_id, str) or not step_id.strip():
raise ValueError("draft start step id must not be blank")
@@ -398,7 +430,7 @@ class WorkflowDraftApi:
state_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None,
outcomes: Sequence[str] | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Replace supplied top-level contract fields in one draft revision.
Complete schema replacement is intentional: deep merging JSON Schema
@@ -445,7 +477,7 @@ class WorkflowDraftApi:
step_id: str,
outcome: str,
target: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.patch_draft_workspace(
workspace_id=workspace_id,
revision=revision,
@@ -469,7 +501,7 @@ class WorkflowDraftApi:
step_id: str,
input_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
input_values: dict[str, Any] = {}
if merge:
workspace = self._workspace_if_revision_matches(
@@ -504,7 +536,7 @@ class WorkflowDraftApi:
step_id: str,
output_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
if merge:
workspace = self._workspace_if_revision_matches(
workspace_id=workspace_id,
@@ -539,7 +571,7 @@ class WorkflowDraftApi:
revision: int,
output_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
output_bindings: list[dict[str, Any]]
workspace: WorkflowDraftWorkspace | None = None
if merge:
+12
View File
@@ -34,6 +34,13 @@ from .deployments import (
ValidateDeploymentResult,
WorkflowDeploymentPayload,
)
from .drafts import (
DeleteDraftWorkspaceResult,
DraftDiagnosticPayload,
DraftWorkspaceResult,
DraftWorkspaceSummary,
ListDraftWorkspacesResult,
)
from .runs import (
InterruptPayload,
InterruptRoutePayload,
@@ -55,8 +62,12 @@ __all__ = [
"CapabilityRefPayload",
"DeleteArtifactResult",
"DeleteDeploymentResult",
"DeleteDraftWorkspaceResult",
"DependencyDiagnosticPayload",
"DeploymentSummary",
"DraftDiagnosticPayload",
"DraftWorkspaceResult",
"DraftWorkspaceSummary",
"HealthResult",
"GuidedResultPayload",
"InterruptPayload",
@@ -65,6 +76,7 @@ __all__ = [
"JsonProjector",
"JsonSchema",
"ListDeploymentsResult",
"ListDraftWorkspacesResult",
"ListArtifactsResult",
"ListRunsResult",
"NextActionPatchExamplePayload",
+53
View File
@@ -0,0 +1,53 @@
"""Canonical result models for persisted draft-workspace operations."""
from typing import Any, Literal, NotRequired, TypedDict
from .common import JsonObject
class DraftDiagnosticPayload(TypedDict):
"""One validation or revision diagnostic attached to a draft workspace."""
code: str
path: str
message: str
step_id: NotRequired[str | None]
repair_hint: NotRequired[str | None]
details: NotRequired[JsonObject]
class DraftWorkspaceSummary(TypedDict):
"""Compact graph facts included with every persisted workspace result."""
# Invalid workspaces preserve malformed authoring values for later repair.
name: Any
start: Any
step_count: int
route_count: int
steps: list[str]
class DraftWorkspaceResult(TypedDict):
"""Canonical persisted workspace envelope returned after reads and edits."""
workspace_id: str
revision: int
title: str | None
status: Literal["valid", "invalid", "conflict"]
diagnostics: list[DraftDiagnosticPayload]
summary: DraftWorkspaceSummary
draft: NotRequired[JsonObject]
class ListDraftWorkspacesResult(TypedDict):
"""All persisted draft-workspace summaries."""
workspaces: list[DraftWorkspaceResult]
class DeleteDraftWorkspaceResult(TypedDict):
"""Outcome of deleting one persisted draft workspace."""
workspace_id: str
deleted: bool
status: Literal["deleted", "not_found"]
+31 -28
View File
@@ -16,8 +16,11 @@ from .drafts import WorkflowDraftApi
from .models import (
DeleteArtifactResult,
DeleteDeploymentResult,
DeleteDraftWorkspaceResult,
DraftWorkspaceResult,
ListArtifactsResult,
ListDeploymentsResult,
ListDraftWorkspacesResult,
ListRunsResult,
RawWorkflowPlan,
RunResult,
@@ -263,7 +266,7 @@ class WorkflowApi:
# -- draft workspaces --
async def list_draft_workspaces(self) -> dict[str, Any]:
async def list_draft_workspaces(self) -> ListDraftWorkspacesResult:
return await self.drafts.list_draft_workspaces()
async def create_draft_workspace(
@@ -272,7 +275,7 @@ class WorkflowApi:
workspace_id: str,
draft: dict[str, Any],
title: str | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.create_draft_workspace(
workspace_id=workspace_id,
draft=draft,
@@ -289,7 +292,7 @@ class WorkflowApi:
state_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None,
outcomes: Sequence[str] = ("ok",),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.create_empty_draft_workspace(
workspace_id=workspace_id,
name=name,
@@ -305,7 +308,7 @@ class WorkflowApi:
*,
workspace_id: str,
include_draft: bool = False,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.get_draft_workspace(
workspace_id=workspace_id,
include_draft=include_draft,
@@ -315,14 +318,14 @@ class WorkflowApi:
self,
*,
workspace_id: str,
) -> dict[str, Any]:
) -> DeleteDraftWorkspaceResult:
return await self.drafts.delete_draft_workspace(workspace_id=workspace_id)
async def validate_draft_workspace(
self,
*,
workspace_id: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.validate_draft_workspace(workspace_id=workspace_id)
async def compile_draft_workspace(
@@ -338,7 +341,7 @@ class WorkflowApi:
workspace_id: str,
revision: int,
patch: list[dict[str, Any]],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.patch_draft_workspace(
workspace_id=workspace_id,
revision=revision,
@@ -351,7 +354,7 @@ class WorkflowApi:
workspace_id: str,
revision: int,
draft: dict[str, Any],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Replace and semantically revalidate one complete workspace draft."""
return await self.drafts.replace_draft_workspace_document(
workspace_id=workspace_id,
@@ -365,7 +368,7 @@ class WorkflowApi:
workspace_id: str,
revision: int,
name: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.set_draft_name(
workspace_id=workspace_id,
revision=revision,
@@ -378,7 +381,7 @@ class WorkflowApi:
workspace_id: str,
revision: int,
step_id: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.set_draft_start(
workspace_id=workspace_id,
revision=revision,
@@ -394,7 +397,7 @@ class WorkflowApi:
state_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None,
outcomes: Sequence[str] | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.set_draft_contract(
workspace_id=workspace_id,
revision=revision,
@@ -412,7 +415,7 @@ class WorkflowApi:
step_id: str,
outcome: str,
target: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.set_draft_route(
workspace_id=workspace_id,
revision=revision,
@@ -429,7 +432,7 @@ class WorkflowApi:
step_id: str,
input_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.set_step_input_map(
workspace_id=workspace_id,
revision=revision,
@@ -445,7 +448,7 @@ class WorkflowApi:
revision: int,
step_id: str,
bindings: Sequence[InputBinding],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.set_step_input_bindings(
workspace_id=workspace_id,
revision=revision,
@@ -460,7 +463,7 @@ class WorkflowApi:
revision: int,
step_id: str,
bindings: Sequence[OutputBinding],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.set_step_output_bindings(
workspace_id=workspace_id,
revision=revision,
@@ -475,7 +478,7 @@ class WorkflowApi:
revision: int,
step_id: str,
update: CapabilityStepUpdate,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Return the updated workspace summary or a revision-conflict payload."""
return await self.draft_authoring.update_capability_step(
workspace_id=workspace_id,
@@ -492,7 +495,7 @@ class WorkflowApi:
step_id: str,
output_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.set_step_output_map(
workspace_id=workspace_id,
revision=revision,
@@ -508,7 +511,7 @@ class WorkflowApi:
revision: int,
output_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.drafts.set_workflow_output_map(
workspace_id=workspace_id,
revision=revision,
@@ -522,7 +525,7 @@ class WorkflowApi:
workspace_id: str,
revision: int,
bindings: Sequence[InputBinding],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.set_workflow_output_bindings(
workspace_id=workspace_id,
revision=revision,
@@ -537,7 +540,7 @@ class WorkflowApi:
step_id: str,
source_path: str,
target_path: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.bind_draft(
workspace_id=workspace_id,
revision=revision,
@@ -562,7 +565,7 @@ class WorkflowApi:
desc: str | None = None,
retry: int | None = None,
timeout_seconds: int | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.add_step_from_capability(
workspace_id=workspace_id,
revision=revision,
@@ -588,7 +591,7 @@ class WorkflowApi:
step: DraftStep,
incoming: RouteSource | None = None,
routes: dict[str, str] | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.add_step(
workspace_id=workspace_id,
revision=revision,
@@ -605,7 +608,7 @@ class WorkflowApi:
revision: int,
step_id: str,
routes: dict[str, str],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.branch_draft(
workspace_id=workspace_id,
revision=revision,
@@ -620,7 +623,7 @@ class WorkflowApi:
revision: int,
branches: list[dict[str, str]],
target: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
refs = [
RouteSource(step_id=b["step_id"], outcome=b["outcome"]) for b in branches
]
@@ -646,7 +649,7 @@ class WorkflowApi:
output_map: dict[str, str] | None = None,
error_message_source: Any | None = None,
title: str | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.create_minimal_draft_workspace(
workspace_id=workspace_id,
name=name,
@@ -669,7 +672,7 @@ class WorkflowApi:
revision: int,
step_id: str,
outcome: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.remove_draft_route(
workspace_id=workspace_id,
revision=revision,
@@ -683,7 +686,7 @@ class WorkflowApi:
workspace_id: str,
revision: int,
step_id: str,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.remove_draft_step(
workspace_id=workspace_id,
revision=revision,
@@ -698,7 +701,7 @@ class WorkflowApi:
step_id: str,
inputs: Sequence[str] = (),
outputs: Sequence[str] = (),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
return await self.draft_authoring.remove_draft_binding(
workspace_id=workspace_id,
revision=revision,
+29 -26
View File
@@ -12,8 +12,11 @@ from .draft_updates import CapabilityStepUpdate
from .models import (
DeleteArtifactResult,
DeleteDeploymentResult,
DeleteDraftWorkspaceResult,
DraftWorkspaceResult,
ListArtifactsResult,
ListDeploymentsResult,
ListDraftWorkspacesResult,
ListRunsResult,
RunResult,
RunTraceResult,
@@ -60,14 +63,14 @@ class WorkflowDraftSurface(Protocol):
not every same-process authoring helper on ``WorkflowApi``.
"""
async def list_draft_workspaces(self) -> dict[str, Any]: ...
async def list_draft_workspaces(self) -> ListDraftWorkspacesResult: ...
async def get_draft_workspace(
self,
*,
workspace_id: str,
include_draft: bool = False,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def create_draft_workspace_from_capability(
self,
@@ -96,7 +99,7 @@ class WorkflowDraftSurface(Protocol):
state_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None,
outcomes: Sequence[str] = ("ok",),
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def patch_draft_workspace(
self,
@@ -104,7 +107,7 @@ class WorkflowDraftSurface(Protocol):
workspace_id: str,
revision: int,
patch: list[dict[str, Any]],
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def replace_draft_workspace_document(
self,
@@ -112,7 +115,7 @@ class WorkflowDraftSurface(Protocol):
workspace_id: str,
revision: int,
draft: dict[str, Any],
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Replace and semantically revalidate one complete workspace draft."""
...
@@ -122,7 +125,7 @@ class WorkflowDraftSurface(Protocol):
workspace_id: str,
revision: int,
name: str,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_draft_start(
self,
@@ -130,7 +133,7 @@ class WorkflowDraftSurface(Protocol):
workspace_id: str,
revision: int,
step_id: str,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_draft_contract(
self,
@@ -141,7 +144,7 @@ class WorkflowDraftSurface(Protocol):
state_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None,
outcomes: Sequence[str] | None = None,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_draft_route(
self,
@@ -151,7 +154,7 @@ class WorkflowDraftSurface(Protocol):
step_id: str,
outcome: str,
target: str,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_step_input_map(
self,
@@ -161,7 +164,7 @@ class WorkflowDraftSurface(Protocol):
step_id: str,
input_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_step_input_bindings(
self,
@@ -170,7 +173,7 @@ class WorkflowDraftSurface(Protocol):
revision: int,
step_id: str,
bindings: Sequence[InputBinding],
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_step_output_bindings(
self,
@@ -179,7 +182,7 @@ class WorkflowDraftSurface(Protocol):
revision: int,
step_id: str,
bindings: Sequence[OutputBinding],
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def update_capability_step(
self,
@@ -188,7 +191,7 @@ class WorkflowDraftSurface(Protocol):
revision: int,
step_id: str,
update: CapabilityStepUpdate,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_step_output_map(
self,
@@ -198,7 +201,7 @@ class WorkflowDraftSurface(Protocol):
step_id: str,
output_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_workflow_output_map(
self,
@@ -207,7 +210,7 @@ class WorkflowDraftSurface(Protocol):
revision: int,
output_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def set_workflow_output_bindings(
self,
@@ -215,7 +218,7 @@ class WorkflowDraftSurface(Protocol):
workspace_id: str,
revision: int,
bindings: Sequence[InputBinding],
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def bind_draft(
self,
@@ -225,7 +228,7 @@ class WorkflowDraftSurface(Protocol):
step_id: str,
source_path: str,
target_path: str,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def add_step_from_capability(
self,
@@ -243,7 +246,7 @@ class WorkflowDraftSurface(Protocol):
desc: str | None = None,
retry: int | None = None,
timeout_seconds: int | None = None,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def add_step(
self,
@@ -254,7 +257,7 @@ class WorkflowDraftSurface(Protocol):
step: DraftStep,
incoming: RouteSource | None = None,
routes: dict[str, str] | None = None,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def branch_draft(
self,
@@ -263,7 +266,7 @@ class WorkflowDraftSurface(Protocol):
revision: int,
step_id: str,
routes: dict[str, str],
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def handle_draft(
self,
@@ -272,7 +275,7 @@ class WorkflowDraftSurface(Protocol):
revision: int,
branches: list[dict[str, str]],
target: str,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def remove_draft_route(
self,
@@ -281,7 +284,7 @@ class WorkflowDraftSurface(Protocol):
revision: int,
step_id: str,
outcome: str,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def remove_draft_step(
self,
@@ -289,7 +292,7 @@ class WorkflowDraftSurface(Protocol):
workspace_id: str,
revision: int,
step_id: str,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def remove_draft_binding(
self,
@@ -299,13 +302,13 @@ class WorkflowDraftSurface(Protocol):
step_id: str,
inputs: Sequence[str] = (),
outputs: Sequence[str] = (),
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def validate_draft_workspace(
self,
*,
workspace_id: str,
) -> dict[str, Any]: ...
) -> DraftWorkspaceResult: ...
async def compile_draft_workspace(
self,
@@ -317,7 +320,7 @@ class WorkflowDraftSurface(Protocol):
self,
*,
workspace_id: str,
) -> dict[str, Any]: ...
) -> DeleteDraftWorkspaceResult: ...
async def create_artifact_from_workspace(
self,
+99 -55
View File
@@ -1,9 +1,14 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, Literal
from typing import Any, Literal, cast
from wf_api import CapabilityStepUpdate
from wf_api.models import (
DeleteDraftWorkspaceResult,
DraftWorkspaceResult,
ListDraftWorkspacesResult,
)
from wf_api.surface import RouteSource
from wf_artifacts.drafts.models import DraftStep
from wf_core.models.steps import InputBinding, OutputBinding
@@ -11,20 +16,33 @@ from wf_core.models.steps import InputBinding, OutputBinding
from .base import RpcCaller
async def _call_draft_workspace(
caller: RpcCaller,
method: str,
params: dict[str, Any],
) -> DraftWorkspaceResult:
"""Call one server-validated draft method with its canonical client type."""
return cast(DraftWorkspaceResult, await caller._call(method, params))
class RpcDraftClientMixin:
"""JSON-RPC implementation of workflow draft workspace surface methods."""
async def list_draft_workspaces(self: RpcCaller) -> dict[str, Any]:
return await self._call("workflow.draft_workspaces.list", {})
async def list_draft_workspaces(self: RpcCaller) -> ListDraftWorkspacesResult:
return cast(
ListDraftWorkspacesResult,
await self._call("workflow.draft_workspaces.list", {}),
)
async def get_draft_workspace(
self: RpcCaller,
*,
workspace_id: str,
include_draft: bool = False,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
"""Return the remote workspace summary or revision-conflict payload."""
return await self._call(
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.get",
{"workspace_id": workspace_id, "include_draft": include_draft},
)
@@ -73,8 +91,9 @@ class RpcDraftClientMixin:
state_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None,
outcomes: Sequence[str] = ("ok",),
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.create_empty",
{
"workspace_id": workspace_id,
@@ -93,8 +112,9 @@ class RpcDraftClientMixin:
workspace_id: str,
revision: int,
patch: list[dict[str, Any]],
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.patch",
{"workspace_id": workspace_id, "revision": revision, "patch": patch},
)
@@ -105,8 +125,9 @@ class RpcDraftClientMixin:
workspace_id: str,
revision: int,
draft: dict[str, Any],
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.replace_document",
{
"workspace_id": workspace_id,
@@ -121,8 +142,9 @@ class RpcDraftClientMixin:
workspace_id: str,
revision: int,
name: str,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_name",
{"workspace_id": workspace_id, "revision": revision, "name": name},
)
@@ -133,8 +155,9 @@ class RpcDraftClientMixin:
workspace_id: str,
revision: int,
step_id: str,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_start",
{"workspace_id": workspace_id, "revision": revision, "step_id": step_id},
)
@@ -148,8 +171,9 @@ class RpcDraftClientMixin:
state_schema: dict[str, Any] | None = None,
output_schema: dict[str, Any] | None = None,
outcomes: Sequence[str] | None = None,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_contract",
{
"workspace_id": workspace_id,
@@ -169,8 +193,9 @@ class RpcDraftClientMixin:
step_id: str,
outcome: str,
target: str,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_route",
{
"workspace_id": workspace_id,
@@ -189,8 +214,9 @@ class RpcDraftClientMixin:
step_id: str,
input_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_step_input_map",
{
"workspace_id": workspace_id,
@@ -208,8 +234,9 @@ class RpcDraftClientMixin:
revision: int,
step_id: str,
bindings: Sequence[InputBinding],
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_step_input_bindings",
{
"workspace_id": workspace_id,
@@ -226,8 +253,9 @@ class RpcDraftClientMixin:
revision: int,
step_id: str,
bindings: Sequence[OutputBinding],
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_step_output_bindings",
{
"workspace_id": workspace_id,
@@ -244,8 +272,9 @@ class RpcDraftClientMixin:
revision: int,
step_id: str,
update: CapabilityStepUpdate,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.update_capability_step",
{
"workspace_id": workspace_id,
@@ -263,8 +292,9 @@ class RpcDraftClientMixin:
step_id: str,
output_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_step_output_map",
{
"workspace_id": workspace_id,
@@ -282,8 +312,9 @@ class RpcDraftClientMixin:
revision: int,
output_map: dict[str, str],
merge: bool = False,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_workflow_output_map",
{
"workspace_id": workspace_id,
@@ -299,8 +330,9 @@ class RpcDraftClientMixin:
workspace_id: str,
revision: int,
bindings: Sequence[InputBinding],
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.set_workflow_output_bindings",
{
"workspace_id": workspace_id,
@@ -317,8 +349,9 @@ class RpcDraftClientMixin:
step_id: str,
source_path: str,
target_path: str,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.bind",
{
"workspace_id": workspace_id,
@@ -345,7 +378,7 @@ class RpcDraftClientMixin:
desc: str | None = None,
retry: int | None = None,
timeout_seconds: int | None = None,
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
if input_map is not None and input_bindings is not None:
raise ValueError("input_map and input_bindings are mutually exclusive")
params: dict[str, object] = {
@@ -370,7 +403,8 @@ class RpcDraftClientMixin:
params["retry"] = retry
if timeout_seconds is not None:
params["timeout_seconds"] = timeout_seconds
return await self._call(
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.add_step_from_capability",
params,
)
@@ -384,8 +418,9 @@ class RpcDraftClientMixin:
step: DraftStep,
incoming: RouteSource | None = None,
routes: dict[str, str] | None = None,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.add_step",
{
"workspace_id": workspace_id,
@@ -408,8 +443,9 @@ class RpcDraftClientMixin:
revision: int,
step_id: str,
routes: dict[str, str],
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.branch",
{
"workspace_id": workspace_id,
@@ -426,8 +462,9 @@ class RpcDraftClientMixin:
revision: int,
branches: list[dict[str, str]],
target: str,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.handle",
{
"workspace_id": workspace_id,
@@ -444,8 +481,9 @@ class RpcDraftClientMixin:
revision: int,
step_id: str,
outcome: str,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.remove_route",
{
"workspace_id": workspace_id,
@@ -461,8 +499,9 @@ class RpcDraftClientMixin:
workspace_id: str,
revision: int,
step_id: str,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.remove_step",
{
"workspace_id": workspace_id,
@@ -479,8 +518,9 @@ class RpcDraftClientMixin:
step_id: str,
inputs: Sequence[str] = (),
outputs: Sequence[str] = (),
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.remove_binding",
{
"workspace_id": workspace_id,
@@ -495,8 +535,9 @@ class RpcDraftClientMixin:
self: RpcCaller,
*,
workspace_id: str,
) -> dict[str, Any]:
return await self._call(
) -> DraftWorkspaceResult:
return await _call_draft_workspace(
self,
"workflow.draft_workspaces.validate",
{"workspace_id": workspace_id},
)
@@ -515,10 +556,13 @@ class RpcDraftClientMixin:
self: RpcCaller,
*,
workspace_id: str,
) -> dict[str, Any]:
return await self._call(
"workflow.draft_workspaces.delete",
{"workspace_id": workspace_id},
) -> DeleteDraftWorkspaceResult:
return cast(
DeleteDraftWorkspaceResult,
await self._call(
"workflow.draft_workspaces.delete",
{"workspace_id": workspace_id},
),
)
async def create_artifact_from_workspace(
+36 -27
View File
@@ -1,9 +1,18 @@
from __future__ import annotations
"""Draft JSON-RPC method registration.
Return annotations stay eagerly evaluated because fastapi-jsonrpc captures them
while registering nested handlers for response validation and OpenRPC output.
"""
from typing import Any
import fastapi_jsonrpc as jsonrpc
from wf_api.models import (
DeleteDraftWorkspaceResult,
DraftWorkspaceResult,
ListDraftWorkspacesResult,
)
from wf_api.surface import RouteSource
from wf_server import WorkflowServer
@@ -83,7 +92,7 @@ def register_methods(
@entrypoint.method(name="workflow.draft_workspaces.list", errors=[WorkflowRpcError])
async def workflow_draft_workspaces_list(
params: ListDraftWorkspacesParams = RpcParams(),
) -> dict[str, Any]:
) -> ListDraftWorkspacesResult:
try:
return await server.api.list_draft_workspaces()
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
@@ -92,7 +101,7 @@ def register_methods(
@entrypoint.method(name="workflow.draft_workspaces.get", errors=[WorkflowRpcError])
async def workflow_draft_workspaces_get(
params: GetDraftWorkspaceParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.get_draft_workspace(
workspace_id=params.workspace_id,
@@ -119,7 +128,7 @@ def register_methods(
)
async def workflow_draft_workspaces_create_empty(
params: CreateEmptyDraftWorkspaceParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.create_empty_draft_workspace(
workspace_id=params.workspace_id,
@@ -138,7 +147,7 @@ def register_methods(
)
async def workflow_draft_workspaces_patch(
params: PatchDraftWorkspaceParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.patch_draft_workspace(
workspace_id=params.workspace_id,
@@ -154,7 +163,7 @@ def register_methods(
)
async def workflow_draft_workspaces_replace_document(
params: ReplaceDraftWorkspaceDocumentParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.replace_draft_workspace_document(
workspace_id=params.workspace_id,
@@ -169,7 +178,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_name(
params: SetDraftNameParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_draft_name(
workspace_id=params.workspace_id,
@@ -184,7 +193,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_start(
params: SetDraftStartParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_draft_start(
workspace_id=params.workspace_id,
@@ -199,7 +208,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_contract(
params: SetDraftContractParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_draft_contract(
workspace_id=params.workspace_id,
@@ -217,7 +226,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_route(
params: SetDraftRouteParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_draft_route(
workspace_id=params.workspace_id,
@@ -235,7 +244,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_step_input_map(
params: SetStepInputMapParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_step_input_map(
workspace_id=params.workspace_id,
@@ -253,7 +262,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_step_input_bindings(
params: SetStepInputBindingsParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_step_input_bindings(
workspace_id=params.workspace_id,
@@ -270,7 +279,7 @@ def register_methods(
)
async def workflow_draft_workspaces_update_capability_step(
params: UpdateCapabilityStepParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.update_capability_step(
workspace_id=params.workspace_id,
@@ -287,7 +296,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_step_output_bindings(
params: SetStepOutputBindingsParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_step_output_bindings(
workspace_id=params.workspace_id,
@@ -304,7 +313,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_step_output_map(
params: SetStepOutputMapParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_step_output_map(
workspace_id=params.workspace_id,
@@ -322,7 +331,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_workflow_output_map(
params: SetWorkflowOutputMapParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_workflow_output_map(
workspace_id=params.workspace_id,
@@ -339,7 +348,7 @@ def register_methods(
)
async def workflow_draft_workspaces_set_workflow_output_bindings(
params: SetWorkflowOutputBindingsParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.set_workflow_output_bindings(
workspace_id=params.workspace_id,
@@ -355,7 +364,7 @@ def register_methods(
)
async def workflow_draft_workspaces_bind(
params: BindDraftParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.bind_draft(
workspace_id=params.workspace_id,
@@ -373,7 +382,7 @@ def register_methods(
)
async def workflow_draft_workspaces_add_step_from_capability(
params: AddStepFromCapabilityParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.add_step_from_capability(
workspace_id=params.workspace_id,
@@ -399,7 +408,7 @@ def register_methods(
)
async def workflow_draft_workspaces_add_step(
params: AddDraftStepParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
incoming = (
None
@@ -425,7 +434,7 @@ def register_methods(
)
async def workflow_draft_workspaces_branch(
params: BranchDraftParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.branch_draft(
workspace_id=params.workspace_id,
@@ -441,7 +450,7 @@ def register_methods(
)
async def workflow_draft_workspaces_handle(
params: HandleDraftParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.handle_draft(
workspace_id=params.workspace_id,
@@ -460,7 +469,7 @@ def register_methods(
)
async def workflow_draft_workspaces_validate(
params: ValidateDraftWorkspaceParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.validate_draft_workspace(
workspace_id=params.workspace_id,
@@ -486,7 +495,7 @@ def register_methods(
)
async def workflow_draft_workspaces_delete(
params: DeleteDraftWorkspaceParams = RpcParams(),
) -> dict[str, Any]:
) -> DeleteDraftWorkspaceResult:
try:
return await server.api.delete_draft_workspace(
workspace_id=params.workspace_id,
@@ -499,7 +508,7 @@ def register_methods(
)
async def workflow_draft_workspaces_remove_route(
params: RemoveDraftRouteParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.remove_draft_route(
workspace_id=params.workspace_id,
@@ -515,7 +524,7 @@ def register_methods(
)
async def workflow_draft_workspaces_remove_step(
params: RemoveDraftStepParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.remove_draft_step(
workspace_id=params.workspace_id,
@@ -530,7 +539,7 @@ def register_methods(
)
async def workflow_draft_workspaces_remove_binding(
params: RemoveDraftBindingParams = RpcParams(),
) -> dict[str, Any]:
) -> DraftWorkspaceResult:
try:
return await server.api.remove_draft_binding(
workspace_id=params.workspace_id,
+22
View File
@@ -760,6 +760,28 @@ async def test_create_draft_workspace_creates_workspace(tmp_path: Path) -> None:
assert fetched["draft"]["steps"]["echo"]["use"] == "demo.personal.echo_tool"
@pytest.mark.asyncio
async def test_invalid_summary_values_remain_inspectable_and_listable(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "invalid_summary_values")
api, _service, _authoring = _draft_api(artifact_store)
draft = _echo_draft()
draft["name"] = 123
draft["start"] = ["echo"]
created = await api.create_draft_workspace(
workspace_id="repairable",
draft=draft,
)
listed = await api.list_draft_workspaces()
assert created["status"] == "invalid"
assert created["summary"]["name"] == 123
assert created["summary"]["start"] == ["echo"]
assert listed["workspaces"][0]["workspace_id"] == "repairable"
@pytest.mark.asyncio
async def test_create_empty_draft_workspace_persists_invalid_skeleton(
tmp_path: Path,
@@ -99,6 +99,109 @@ def test_openrpc_exposes_typed_artifact_results(
)
@pytest.mark.parametrize(
"method_name",
[
"workflow.draft_workspaces.get",
"workflow.draft_workspaces.create_empty",
"workflow.draft_workspaces.patch",
"workflow.draft_workspaces.replace_document",
"workflow.draft_workspaces.set_name",
"workflow.draft_workspaces.set_start",
"workflow.draft_workspaces.set_contract",
"workflow.draft_workspaces.set_route",
"workflow.draft_workspaces.set_step_input_map",
"workflow.draft_workspaces.set_step_input_bindings",
"workflow.draft_workspaces.update_capability_step",
"workflow.draft_workspaces.set_step_output_bindings",
"workflow.draft_workspaces.set_step_output_map",
"workflow.draft_workspaces.set_workflow_output_map",
"workflow.draft_workspaces.set_workflow_output_bindings",
"workflow.draft_workspaces.bind",
"workflow.draft_workspaces.add_step_from_capability",
"workflow.draft_workspaces.add_step",
"workflow.draft_workspaces.branch",
"workflow.draft_workspaces.handle",
"workflow.draft_workspaces.validate",
"workflow.draft_workspaces.remove_route",
"workflow.draft_workspaces.remove_step",
"workflow.draft_workspaces.remove_binding",
],
)
def test_openrpc_exposes_typed_draft_workspace_results(
openrpc_document: dict[str, Any],
method_name: str,
) -> None:
_assert_result_component(
openrpc_document,
method_name=method_name,
component_name="DraftWorkspaceResult",
properties={
"workspace_id",
"revision",
"title",
"status",
"diagnostics",
"summary",
},
)
def test_openrpc_exposes_typed_draft_workspace_list_result(
openrpc_document: dict[str, Any],
) -> None:
_assert_result_component(
openrpc_document,
method_name="workflow.draft_workspaces.list",
component_name="ListDraftWorkspacesResult",
properties={"workspaces"},
)
def test_openrpc_exposes_typed_delete_draft_workspace_result(
openrpc_document: dict[str, Any],
) -> None:
_assert_result_component(
openrpc_document,
method_name="workflow.draft_workspaces.delete",
component_name="DeleteDraftWorkspaceResult",
properties={"workspace_id", "deleted", "status"},
)
def test_openrpc_pins_nested_draft_workspace_contract(
openrpc_document: dict[str, Any],
) -> None:
schemas = openrpc_document["components"]["schemas"]
workspace = schemas["DraftWorkspaceResult"]
diagnostic = schemas["DraftDiagnosticPayload"]
listed = schemas["ListDraftWorkspacesResult"]
deleted = schemas["DeleteDraftWorkspaceResult"]
assert workspace["required"] == [
"workspace_id",
"revision",
"title",
"status",
"diagnostics",
"summary",
]
assert "draft" not in workspace["required"]
assert workspace["properties"]["status"]["enum"] == [
"valid",
"invalid",
"conflict",
]
assert workspace["properties"]["diagnostics"]["items"] == {
"$ref": "#/components/schemas/DraftDiagnosticPayload"
}
assert diagnostic["required"] == ["code", "path", "message"]
assert listed["properties"]["workspaces"]["items"] == {
"$ref": "#/components/schemas/DraftWorkspaceResult"
}
assert deleted["properties"]["status"]["enum"] == ["deleted", "not_found"]
@pytest.mark.parametrize(
("method_name", "component_name", "properties"),
[