feat: add semantic draft authoring operations
This commit is contained in:
@@ -18,6 +18,7 @@ from wf_platform import CapabilitySource
|
||||
from .artifact_plans import raw_plan_from_artifact
|
||||
from .artifact_refs import artifact_capability_id
|
||||
from .capability_requirements import required_capability_payloads
|
||||
from .draft_authoring import WorkflowDraftAuthoringApi
|
||||
from .drafts import WorkflowDraftApi
|
||||
from .listing import matches_query, paged_list_payload
|
||||
from .next_actions import NextActions
|
||||
@@ -64,6 +65,7 @@ class WorkflowCapabilityApi:
|
||||
def __init__(self, context: WorkflowOperationContext) -> None:
|
||||
self.context = context
|
||||
self.drafts = WorkflowDraftApi(context)
|
||||
self.draft_authoring = WorkflowDraftAuthoringApi(context, self.drafts)
|
||||
|
||||
async def list_capabilities(
|
||||
self,
|
||||
@@ -353,7 +355,7 @@ class WorkflowCapabilityApi:
|
||||
"""Create a patchable draft workspace from inspect_capability hints."""
|
||||
capability = await self.inspect_capability(qualified_name=capability_name)
|
||||
hints = capability["wrapper_hints"]
|
||||
result = await self.drafts.create_minimal_draft_workspace(
|
||||
result = await self.draft_authoring.create_minimal_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
name=name or _draft_name_from_capability(capability_name),
|
||||
capability_name=capability_name,
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from wf_core.models.steps import (
|
||||
InputBinding,
|
||||
OutputBinding,
|
||||
)
|
||||
from wf_core.paths import GraphSourcePath
|
||||
|
||||
from .constants import (
|
||||
DEFAULT_CALL_STEP_ID,
|
||||
DEFAULT_ERROR_OUTCOME,
|
||||
DEFAULT_ERROR_STEP_ID,
|
||||
DEFAULT_OK_OUTCOME,
|
||||
RUNTIME_ERROR_CAPABILITY,
|
||||
)
|
||||
from .draft_payloads import (
|
||||
_graph_path_payload,
|
||||
draft_step,
|
||||
escape_json_pointer,
|
||||
input_bindings_payload,
|
||||
output_bindings_payload,
|
||||
state_root_field,
|
||||
)
|
||||
from .drafts import (
|
||||
WorkflowDraftApi,
|
||||
_draft_input_maps,
|
||||
_draft_output_map,
|
||||
)
|
||||
from .operation_context import WorkflowOperationContext
|
||||
from .schema_projection import project_output_property_to_state_schema
|
||||
|
||||
|
||||
class WorkflowDraftAuthoringApi:
|
||||
"""Capability-aware semantic edits over revisioned workflow drafts."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: WorkflowOperationContext,
|
||||
drafts: WorkflowDraftApi,
|
||||
) -> None:
|
||||
self.context = context
|
||||
self.drafts = drafts
|
||||
|
||||
def _outcomes_for_capability(self, qualified_name: str) -> tuple[str, ...] | None:
|
||||
try:
|
||||
spec = self.context.specs.get_qualified_spec(qualified_name)
|
||||
except KeyError:
|
||||
return None
|
||||
outcomes = getattr(spec, "outcomes", None)
|
||||
return tuple(outcomes) if outcomes is not None else None
|
||||
|
||||
async def create_minimal_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
name: str,
|
||||
capability_name: str,
|
||||
input_schema: dict[str, Any],
|
||||
state_schema: dict[str, Any],
|
||||
output_schema: dict[str, Any],
|
||||
input: Sequence[InputBinding] | None = None,
|
||||
output: Sequence[OutputBinding] | None = None,
|
||||
input_map: dict[str, str] | None = None,
|
||||
output_map: dict[str, str] | None = None,
|
||||
error_message_source: str | GraphSourcePath | None = None,
|
||||
title: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Bootstrap the smallest patchable draft around one workflow capability."""
|
||||
draft_input, draft_with = _draft_input_maps(
|
||||
input=input,
|
||||
input_map=input_map,
|
||||
)
|
||||
draft_output = _draft_output_map(output=output, output_map=output_map)
|
||||
outcomes = self._outcomes_for_capability(capability_name) or (
|
||||
DEFAULT_OK_OUTCOME,
|
||||
)
|
||||
steps: dict[str, Any] = {
|
||||
DEFAULT_CALL_STEP_ID: {
|
||||
"use": capability_name,
|
||||
"input": input_bindings_payload(draft_input, draft_with),
|
||||
"output": output_bindings_payload(draft_output),
|
||||
}
|
||||
}
|
||||
routes: dict[str, dict[str, str]] = {
|
||||
DEFAULT_CALL_STEP_ID: {DEFAULT_OK_OUTCOME: "__end__"}
|
||||
}
|
||||
if DEFAULT_ERROR_OUTCOME in outcomes:
|
||||
error_input: dict[str, Any] = {
|
||||
"target": "message",
|
||||
"value": "Capability call failed",
|
||||
}
|
||||
if error_message_source is not None:
|
||||
error_input = {
|
||||
"target": "message",
|
||||
"path": _graph_path_payload(error_message_source),
|
||||
}
|
||||
steps[DEFAULT_ERROR_STEP_ID] = {
|
||||
"use": RUNTIME_ERROR_CAPABILITY,
|
||||
"input": [error_input],
|
||||
"output": [],
|
||||
}
|
||||
routes[DEFAULT_CALL_STEP_ID][DEFAULT_ERROR_OUTCOME] = DEFAULT_ERROR_STEP_ID
|
||||
routes[DEFAULT_ERROR_STEP_ID] = {DEFAULT_OK_OUTCOME: "__end__"}
|
||||
draft = {
|
||||
"name": name,
|
||||
"input_schema": input_schema,
|
||||
"state_schema": state_schema,
|
||||
"output_schema": output_schema,
|
||||
"start": DEFAULT_CALL_STEP_ID,
|
||||
"steps": steps,
|
||||
"routes": routes,
|
||||
}
|
||||
return await self.drafts.create_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
title=title,
|
||||
draft=draft,
|
||||
)
|
||||
|
||||
async def bind_output_to_state(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
output_field: str,
|
||||
state_path: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Declare a state field from a step output and bind that output to it.
|
||||
|
||||
This is the common draft-authoring repair for validation errors where a
|
||||
step writes to ``state.x`` before ``state_schema.properties.x`` exists.
|
||||
It deliberately edits only one root state field and one step output map.
|
||||
Route changes remain explicit through ``set_draft_route``.
|
||||
"""
|
||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||
step = draft_step(workspace.draft, step_id)
|
||||
capability_name = step.get("use")
|
||||
if not isinstance(capability_name, str) or not capability_name:
|
||||
raise ValueError(
|
||||
f"draft step {step_id!r} does not declare a capability use"
|
||||
)
|
||||
|
||||
state_field = state_root_field(state_path)
|
||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
||||
output_schema = (
|
||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||
)
|
||||
state_schema = workspace.draft.get("state_schema", {})
|
||||
if not isinstance(state_schema, dict):
|
||||
raise ValueError("draft state_schema must be an object")
|
||||
projected = project_output_property_to_state_schema(
|
||||
state_schema=state_schema,
|
||||
output_schema=output_schema,
|
||||
output_field=output_field,
|
||||
state_field=state_field,
|
||||
)
|
||||
output_map = {
|
||||
**self.drafts._step_output_map(workspace_id=workspace_id, step_id=step_id),
|
||||
output_field: state_path,
|
||||
}
|
||||
return await self.drafts.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/state_schema",
|
||||
"value": projected,
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": f"/steps/{escape_json_pointer(step_id)}/output",
|
||||
"value": output_bindings_payload(output_map),
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
async def add_step_from_capability(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
capability_name: str,
|
||||
route_from_step: str | None = None,
|
||||
route_from_outcome: str = DEFAULT_OK_OUTCOME,
|
||||
routes: dict[str, str] | None = None,
|
||||
input_map: dict[str, str] | None = None,
|
||||
bind_outputs: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Add one capability step plus explicit route/map/schema wiring.
|
||||
|
||||
This is a composed authoring helper for agents. It edits the draft in
|
||||
one revision so callers do not have to interleave add-step, route,
|
||||
input-map, state-schema, and output-map operations by hand.
|
||||
"""
|
||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||
steps = workspace.draft.get("steps")
|
||||
if not isinstance(steps, dict):
|
||||
raise ValueError("draft steps must be an object")
|
||||
if step_id in steps:
|
||||
raise ValueError(f"draft step {step_id!r} already exists")
|
||||
|
||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
||||
output_schema = (
|
||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||
)
|
||||
state_schema = workspace.draft.get("state_schema", {})
|
||||
if not isinstance(state_schema, dict):
|
||||
raise ValueError("draft state_schema must be an object")
|
||||
|
||||
declared_outcomes = self._outcomes_for_capability(capability_name)
|
||||
if declared_outcomes is None:
|
||||
declared_outcomes = (DEFAULT_OK_OUTCOME,)
|
||||
|
||||
if routes is not None:
|
||||
missing_outcomes = set(declared_outcomes) - set(routes.keys())
|
||||
unknown_outcomes = set(routes.keys()) - set(declared_outcomes)
|
||||
if missing_outcomes or unknown_outcomes:
|
||||
details = [
|
||||
f"declared_outcomes={declared_outcomes!r}",
|
||||
f"missing_outcomes={sorted(missing_outcomes)!r}",
|
||||
f"unknown_outcomes={sorted(unknown_outcomes)!r}",
|
||||
]
|
||||
raise ValueError(
|
||||
f"capability {capability_name!r} declares outcomes "
|
||||
f"{declared_outcomes}, but routes has "
|
||||
f"missing routes {sorted(missing_outcomes)} and unknown "
|
||||
f"routes {sorted(unknown_outcomes)}; " + ", ".join(details)
|
||||
)
|
||||
step_routes = dict(routes)
|
||||
else:
|
||||
if len(declared_outcomes) == 1:
|
||||
step_routes = {declared_outcomes[0]: "__end__"}
|
||||
else:
|
||||
missing_outcomes = sorted(declared_outcomes)
|
||||
raise ValueError(
|
||||
f"capability {capability_name!r} declares outcomes "
|
||||
f"{declared_outcomes} with no routes supplied; missing "
|
||||
f"routes for {missing_outcomes}"
|
||||
)
|
||||
|
||||
input_map = input_map or {}
|
||||
bind_outputs = bind_outputs or {}
|
||||
projected_state_schema = state_schema
|
||||
for output_field, path in bind_outputs.items():
|
||||
sf = state_root_field(path)
|
||||
projected_state_schema = project_output_property_to_state_schema(
|
||||
state_schema=projected_state_schema,
|
||||
output_schema=output_schema,
|
||||
output_field=output_field,
|
||||
state_field=sf,
|
||||
)
|
||||
|
||||
patch: list[dict[str, Any]] = [
|
||||
{
|
||||
"op": "add",
|
||||
"path": f"/steps/{escape_json_pointer(step_id)}",
|
||||
"value": {
|
||||
"use": capability_name,
|
||||
"input": input_bindings_payload(input_map, {}),
|
||||
"output": output_bindings_payload(bind_outputs),
|
||||
},
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": f"/routes/{escape_json_pointer(step_id)}",
|
||||
"value": step_routes,
|
||||
},
|
||||
]
|
||||
if projected_state_schema != state_schema:
|
||||
patch.insert(
|
||||
0,
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/state_schema",
|
||||
"value": projected_state_schema,
|
||||
},
|
||||
)
|
||||
if route_from_step is not None:
|
||||
patch.append(
|
||||
{
|
||||
"op": "add",
|
||||
"path": (
|
||||
f"/routes/{escape_json_pointer(route_from_step)}/"
|
||||
f"{escape_json_pointer(route_from_outcome)}"
|
||||
),
|
||||
"value": step_id,
|
||||
}
|
||||
)
|
||||
|
||||
return await self.drafts.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=patch,
|
||||
)
|
||||
|
||||
async def branch_draft(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
routes: dict[str, str],
|
||||
) -> dict[str, Any]:
|
||||
"""Atomically set routes for one step, preserving unspecified outcomes."""
|
||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||
draft_routes = workspace.draft.get("routes", {})
|
||||
if not isinstance(draft_routes, dict):
|
||||
raise ValueError("draft routes must be an object")
|
||||
existing = draft_routes.get(step_id, {})
|
||||
if not isinstance(existing, dict):
|
||||
raise ValueError(f"routes for step {step_id!r} must be an object")
|
||||
merged = {**existing, **routes}
|
||||
if merged == existing:
|
||||
return await self.drafts.get_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
return await self.drafts.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": f"/routes/{escape_json_pointer(step_id)}",
|
||||
"value": merged,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
async def handle_draft(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
branches: Sequence[DraftOutcomeRef],
|
||||
target: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Update the target for multiple (step, outcome) pairs atomically."""
|
||||
if not branches:
|
||||
return await self.drafts.get_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||
draft_routes = workspace.draft.get("routes", {})
|
||||
if not isinstance(draft_routes, dict):
|
||||
raise ValueError("draft routes must be an object")
|
||||
patch: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
for ref in branches:
|
||||
key = (ref.step_id, ref.outcome)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
step_routes = draft_routes.get(ref.step_id, {})
|
||||
if not isinstance(step_routes, dict):
|
||||
continue
|
||||
if ref.outcome not in step_routes:
|
||||
continue
|
||||
if step_routes[ref.outcome] == target:
|
||||
continue
|
||||
patch.append(
|
||||
{
|
||||
"op": "replace",
|
||||
"path": (
|
||||
f"/routes/{escape_json_pointer(ref.step_id)}/"
|
||||
f"{escape_json_pointer(ref.outcome)}"
|
||||
),
|
||||
"value": target,
|
||||
}
|
||||
)
|
||||
if not patch:
|
||||
return await self.drafts.get_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
return await self.drafts.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=patch,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DraftOutcomeRef:
|
||||
"""A reference to a specific outcome of a draft step."""
|
||||
|
||||
step_id: str
|
||||
outcome: str
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
|
||||
|
||||
|
||||
def draft_step(draft: Mapping[str, Any], step_id: str) -> Mapping[str, Any]:
|
||||
"""Return one step mapping from a draft, raising on missing or non-object."""
|
||||
steps = draft.get("steps", {})
|
||||
if not isinstance(steps, Mapping):
|
||||
raise KeyError("draft steps are not available")
|
||||
step = steps[step_id]
|
||||
if not isinstance(step, Mapping):
|
||||
raise KeyError(f"draft step {step_id!r} is not an object")
|
||||
return step
|
||||
|
||||
|
||||
def escape_json_pointer(value: str) -> str:
|
||||
"""Escape one JSON Pointer path segment for generated JSON Patch helpers."""
|
||||
return value.replace("~", "~0").replace("/", "~1")
|
||||
|
||||
|
||||
def input_bindings_payload(
|
||||
input_map: dict[str, str],
|
||||
input_values: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Serialize draft input maps into canonical string-path binding payloads."""
|
||||
return [
|
||||
{"target": _local_path_payload(target), "value": value}
|
||||
for target, value in input_values.items()
|
||||
] + [
|
||||
{"target": _local_path_payload(target), "path": _graph_path_payload(source)}
|
||||
for source, target in input_map.items()
|
||||
]
|
||||
|
||||
|
||||
def output_bindings_payload(output_map: dict[str, str]) -> list[dict[str, Any]]:
|
||||
"""Serialize draft output maps into canonical string-path binding payloads."""
|
||||
return [
|
||||
{"source": _local_path_payload(source), "target": _state_path_payload(target)}
|
||||
for source, target in output_map.items()
|
||||
]
|
||||
|
||||
|
||||
def state_root_field(value: str) -> str:
|
||||
"""Return the single root field name from a state path, or raise."""
|
||||
path = StatePath.parse(value)
|
||||
if len(path.parts) != 1:
|
||||
raise ValueError("state_path must name one root field, such as state.after")
|
||||
return path.parts[0]
|
||||
|
||||
|
||||
def _local_path_payload(value: str) -> str:
|
||||
return LocalPath._serialize(LocalPath.parse(value))
|
||||
|
||||
|
||||
def _graph_path_payload(value: str | GraphSourcePath) -> str:
|
||||
path = value if isinstance(value, GraphSourcePath) else GraphSourcePath.parse(value)
|
||||
return GraphSourcePath._serialize(path)
|
||||
|
||||
|
||||
def _state_path_payload(value: str) -> str:
|
||||
return StatePath._serialize(StatePath.parse(value))
|
||||
+19
-326
@@ -25,21 +25,24 @@ from wf_core.models.steps import (
|
||||
InputValueBinding,
|
||||
OutputBinding,
|
||||
)
|
||||
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
|
||||
|
||||
from .capability_requirements import (
|
||||
required_capabilities_for_plan,
|
||||
required_capability_payloads,
|
||||
)
|
||||
from .constants import (
|
||||
DEFAULT_CALL_STEP_ID,
|
||||
DEFAULT_ERROR_OUTCOME,
|
||||
DEFAULT_ERROR_STEP_ID,
|
||||
DEFAULT_OK_OUTCOME,
|
||||
RUNTIME_ERROR_CAPABILITY,
|
||||
from .draft_payloads import (
|
||||
draft_step as _draft_step,
|
||||
)
|
||||
from .draft_payloads import (
|
||||
escape_json_pointer as _escape_json_pointer,
|
||||
)
|
||||
from .draft_payloads import (
|
||||
input_bindings_payload as _draft_input_bindings_payload,
|
||||
)
|
||||
from .draft_payloads import (
|
||||
output_bindings_payload as _draft_output_bindings_payload,
|
||||
)
|
||||
from .operation_context import WorkflowOperationContext
|
||||
from .schema_projection import project_output_property_to_state_schema
|
||||
|
||||
|
||||
class WorkflowDraftApi:
|
||||
@@ -181,6 +184,14 @@ class WorkflowDraftApi:
|
||||
store.save_workspace(refreshed)
|
||||
return 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."""
|
||||
workspace = self._draft_store().get_workspace(workspace_id)
|
||||
validation = await self.validate_draft(draft=workspace.draft)
|
||||
if validation["status"] != "valid":
|
||||
return validation
|
||||
return await self.compile_draft(draft=workspace.draft)
|
||||
|
||||
async def patch_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
@@ -288,197 +299,6 @@ class WorkflowDraftApi:
|
||||
],
|
||||
)
|
||||
|
||||
async def add_state_schema_from_output(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
output_field: str,
|
||||
state_path: str,
|
||||
) -> dict[str, Any]:
|
||||
workspace = self._draft_store().get_workspace(workspace_id)
|
||||
step = _draft_step(workspace.draft, step_id)
|
||||
capability_name = step.get("use")
|
||||
if not isinstance(capability_name, str) or not capability_name:
|
||||
raise ValueError(
|
||||
f"draft step {step_id!r} does not declare a capability use"
|
||||
)
|
||||
state_field = _state_root_field(state_path)
|
||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
||||
output_schema = (
|
||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||
)
|
||||
state_schema = workspace.draft.get("state_schema", {})
|
||||
if not isinstance(state_schema, dict):
|
||||
raise ValueError("draft state_schema must be an object")
|
||||
projected = project_output_property_to_state_schema(
|
||||
state_schema=state_schema,
|
||||
output_schema=output_schema,
|
||||
output_field=output_field,
|
||||
state_field=state_field,
|
||||
)
|
||||
return await self.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/state_schema",
|
||||
"value": projected,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
async def bind_output_to_state(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
output_field: str,
|
||||
state_path: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Declare a state field from a step output and bind that output to it.
|
||||
|
||||
This is the common draft-authoring repair for validation errors where a
|
||||
step writes to ``state.x`` before ``state_schema.properties.x`` exists.
|
||||
It deliberately edits only one root state field and one step output map.
|
||||
Route changes remain explicit through ``set_draft_route``.
|
||||
"""
|
||||
workspace = self._draft_store().get_workspace(workspace_id)
|
||||
step = _draft_step(workspace.draft, step_id)
|
||||
capability_name = step.get("use")
|
||||
if not isinstance(capability_name, str) or not capability_name:
|
||||
raise ValueError(
|
||||
f"draft step {step_id!r} does not declare a capability use"
|
||||
)
|
||||
|
||||
state_field = _state_root_field(state_path)
|
||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
||||
output_schema = (
|
||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||
)
|
||||
state_schema = workspace.draft.get("state_schema", {})
|
||||
if not isinstance(state_schema, dict):
|
||||
raise ValueError("draft state_schema must be an object")
|
||||
projected = project_output_property_to_state_schema(
|
||||
state_schema=state_schema,
|
||||
output_schema=output_schema,
|
||||
output_field=output_field,
|
||||
state_field=state_field,
|
||||
)
|
||||
output_map = {
|
||||
**self._step_output_map(workspace_id=workspace_id, step_id=step_id),
|
||||
output_field: state_path,
|
||||
}
|
||||
return await self.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/state_schema",
|
||||
"value": projected,
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": f"/steps/{_escape_json_pointer(step_id)}/output",
|
||||
"value": _draft_output_bindings_payload(output_map),
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
async def add_step_from_capability(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
capability_name: str,
|
||||
route_from_step: str | None = None,
|
||||
route_from_outcome: str = DEFAULT_OK_OUTCOME,
|
||||
route_outcome: str = DEFAULT_OK_OUTCOME,
|
||||
route_to: str = "__end__",
|
||||
input_map: dict[str, str] | None = None,
|
||||
bind_outputs: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Add one capability step plus explicit route/map/schema wiring.
|
||||
|
||||
This is a composed authoring helper for agents. It edits the draft in
|
||||
one revision so callers do not have to interleave add-step, route,
|
||||
input-map, state-schema, and output-map operations by hand.
|
||||
"""
|
||||
workspace = self._draft_store().get_workspace(workspace_id)
|
||||
steps = workspace.draft.get("steps")
|
||||
if not isinstance(steps, dict):
|
||||
raise ValueError("draft steps must be an object")
|
||||
if step_id in steps:
|
||||
raise ValueError(f"draft step {step_id!r} already exists")
|
||||
|
||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
||||
output_schema = (
|
||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||
)
|
||||
state_schema = workspace.draft.get("state_schema", {})
|
||||
if not isinstance(state_schema, dict):
|
||||
raise ValueError("draft state_schema must be an object")
|
||||
|
||||
input_map = input_map or {}
|
||||
bind_outputs = bind_outputs or {}
|
||||
projected_state_schema = state_schema
|
||||
for output_field, state_path in bind_outputs.items():
|
||||
state_field = _state_root_field(state_path)
|
||||
projected_state_schema = project_output_property_to_state_schema(
|
||||
state_schema=projected_state_schema,
|
||||
output_schema=output_schema,
|
||||
output_field=output_field,
|
||||
state_field=state_field,
|
||||
)
|
||||
|
||||
patch: list[dict[str, Any]] = [
|
||||
{
|
||||
"op": "add",
|
||||
"path": f"/steps/{_escape_json_pointer(step_id)}",
|
||||
"value": {
|
||||
"use": capability_name,
|
||||
"input": _draft_input_bindings_payload(input_map, {}),
|
||||
"output": _draft_output_bindings_payload(bind_outputs),
|
||||
},
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": f"/routes/{_escape_json_pointer(step_id)}",
|
||||
"value": {route_outcome: route_to},
|
||||
},
|
||||
]
|
||||
if projected_state_schema != state_schema:
|
||||
patch.insert(
|
||||
0,
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/state_schema",
|
||||
"value": projected_state_schema,
|
||||
},
|
||||
)
|
||||
if route_from_step is not None:
|
||||
patch.append(
|
||||
{
|
||||
"op": "add",
|
||||
"path": (
|
||||
f"/routes/{_escape_json_pointer(route_from_step)}/"
|
||||
f"{_escape_json_pointer(route_from_outcome)}"
|
||||
),
|
||||
"value": step_id,
|
||||
}
|
||||
)
|
||||
|
||||
return await self.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=patch,
|
||||
)
|
||||
|
||||
def _step_input_maps(
|
||||
self,
|
||||
*,
|
||||
@@ -494,76 +314,6 @@ class WorkflowDraftApi:
|
||||
step = _draft_step(workspace.draft, step_id)
|
||||
return _output_map_from_payload(step.get("output", []))
|
||||
|
||||
async def create_minimal_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
name: str,
|
||||
capability_name: str,
|
||||
input_schema: dict[str, Any],
|
||||
state_schema: dict[str, Any],
|
||||
output_schema: dict[str, Any],
|
||||
input: Sequence[InputBinding] | None = None,
|
||||
output: Sequence[OutputBinding] | None = None,
|
||||
input_map: dict[str, str] | None = None,
|
||||
output_map: dict[str, str] | None = None,
|
||||
error_message_source: str | GraphSourcePath | None = None,
|
||||
title: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Bootstrap the smallest patchable draft around one workflow capability."""
|
||||
draft_input, draft_with = _draft_input_maps(
|
||||
input=input,
|
||||
input_map=input_map,
|
||||
)
|
||||
draft_output = _draft_output_map(output=output, output_map=output_map)
|
||||
outcomes = self._outcomes_for_capability(capability_name) or (
|
||||
DEFAULT_OK_OUTCOME,
|
||||
)
|
||||
steps: dict[str, Any] = {
|
||||
DEFAULT_CALL_STEP_ID: {
|
||||
"use": capability_name,
|
||||
"input": _draft_input_bindings_payload(draft_input, draft_with),
|
||||
"output": _draft_output_bindings_payload(draft_output),
|
||||
}
|
||||
}
|
||||
routes: dict[str, dict[str, str]] = {
|
||||
DEFAULT_CALL_STEP_ID: {DEFAULT_OK_OUTCOME: "__end__"}
|
||||
}
|
||||
if DEFAULT_ERROR_OUTCOME in outcomes:
|
||||
# The bootstrapper cannot infer provider-specific error envelopes.
|
||||
# Use a static default unless the caller explicitly supplies the
|
||||
# state path containing a better provider error message.
|
||||
error_input: dict[str, Any] = {
|
||||
"target": {"root": "local", "parts": ["message"]},
|
||||
"value": "Capability call failed",
|
||||
}
|
||||
if error_message_source is not None:
|
||||
error_input = {
|
||||
"target": {"root": "local", "parts": ["message"]},
|
||||
"path": _graph_path_payload(error_message_source),
|
||||
}
|
||||
steps[DEFAULT_ERROR_STEP_ID] = {
|
||||
"use": RUNTIME_ERROR_CAPABILITY,
|
||||
"input": [error_input],
|
||||
"output": [],
|
||||
}
|
||||
routes[DEFAULT_CALL_STEP_ID][DEFAULT_ERROR_OUTCOME] = DEFAULT_ERROR_STEP_ID
|
||||
routes[DEFAULT_ERROR_STEP_ID] = {DEFAULT_OK_OUTCOME: "__end__"}
|
||||
draft = {
|
||||
"name": name,
|
||||
"input_schema": input_schema,
|
||||
"state_schema": state_schema,
|
||||
"output_schema": output_schema,
|
||||
"start": DEFAULT_CALL_STEP_ID,
|
||||
"steps": steps,
|
||||
"routes": routes,
|
||||
}
|
||||
return await self.create_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
title=title,
|
||||
draft=draft,
|
||||
)
|
||||
|
||||
|
||||
def _draft_input_maps(
|
||||
*,
|
||||
@@ -606,38 +356,6 @@ def _draft_output_map(
|
||||
return {str(binding.source): str(binding.target) for binding in output}
|
||||
|
||||
|
||||
def _draft_input_bindings_payload(
|
||||
input_map: dict[str, str],
|
||||
input_values: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Serialize draft input maps into canonical structural binding payloads."""
|
||||
return [
|
||||
{"target": _local_path_payload(target), "value": value}
|
||||
for target, value in input_values.items()
|
||||
] + [
|
||||
{"target": _local_path_payload(target), "path": _graph_path_payload(source)}
|
||||
for source, target in input_map.items()
|
||||
]
|
||||
|
||||
|
||||
def _draft_output_bindings_payload(output_map: dict[str, str]) -> list[dict[str, Any]]:
|
||||
"""Serialize draft output maps into canonical structural binding payloads."""
|
||||
return [
|
||||
{"source": _local_path_payload(source), "target": _state_path_payload(target)}
|
||||
for source, target in output_map.items()
|
||||
]
|
||||
|
||||
|
||||
def _draft_step(draft: Mapping[str, Any], step_id: str) -> Mapping[str, Any]:
|
||||
steps = draft.get("steps", {})
|
||||
if not isinstance(steps, Mapping):
|
||||
raise KeyError("draft steps are not available")
|
||||
step = steps[step_id]
|
||||
if not isinstance(step, Mapping):
|
||||
raise KeyError(f"draft step {step_id!r} is not an object")
|
||||
return step
|
||||
|
||||
|
||||
def _input_maps_from_payload(
|
||||
payload: Any,
|
||||
) -> tuple[dict[str, str], dict[str, Any]]:
|
||||
@@ -694,24 +412,6 @@ def _path_text(value: Any, *, expected_root: str | None = None) -> str:
|
||||
return root if not raw_parts else f"{root}.{'.'.join(raw_parts)}"
|
||||
|
||||
|
||||
def _graph_path_payload(value: str | GraphSourcePath) -> str:
|
||||
path = value if isinstance(value, GraphSourcePath) else GraphSourcePath.parse(value)
|
||||
return GraphSourcePath._serialize(path)
|
||||
|
||||
|
||||
def _local_path_payload(value: str) -> str:
|
||||
return LocalPath._serialize(LocalPath.parse(value))
|
||||
|
||||
|
||||
def _state_path_payload(value: str) -> str:
|
||||
return StatePath._serialize(StatePath.parse(value))
|
||||
|
||||
|
||||
def _escape_json_pointer(value: str) -> str:
|
||||
"""Escape one JSON Pointer path segment for generated JSON Patch helpers."""
|
||||
return value.replace("~", "~0").replace("/", "~1")
|
||||
|
||||
|
||||
def _with_workspace_repair_hints(
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
@@ -762,10 +462,3 @@ def _draft_repair_hint(
|
||||
f"wf draft bind-output-to-state {workspace_id} --revision {revision} "
|
||||
f"--step {step_id} --output {output_field} --state {state_path}"
|
||||
)
|
||||
|
||||
|
||||
def _state_root_field(value: str) -> str:
|
||||
path = StatePath.parse(value)
|
||||
if len(path.parts) != 1:
|
||||
raise ValueError("state_path must name one root field, such as state.after")
|
||||
return path.parts[0]
|
||||
|
||||
+50
-24
@@ -8,6 +8,7 @@ from wf_artifacts import ArtifactKind
|
||||
from .artifacts import WorkflowArtifactApi
|
||||
from .capabilities import WorkflowCapabilityApi
|
||||
from .deployments import WorkflowDeploymentApi
|
||||
from .draft_authoring import WorkflowDraftAuthoringApi
|
||||
from .drafts import WorkflowDraftApi
|
||||
from .models import RawWorkflowPlan
|
||||
from .operation_context import WorkflowOperationContext
|
||||
@@ -26,6 +27,7 @@ class WorkflowApi:
|
||||
self.context = context
|
||||
self.capabilities = WorkflowCapabilityApi(context)
|
||||
self.drafts = WorkflowDraftApi(context)
|
||||
self.draft_authoring = WorkflowDraftAuthoringApi(context, self.drafts)
|
||||
self.artifacts = WorkflowArtifactApi(context)
|
||||
self.deployments = WorkflowDeploymentApi(context)
|
||||
self.runs = WorkflowRunApi(context)
|
||||
@@ -285,6 +287,13 @@ class WorkflowApi:
|
||||
) -> dict[str, Any]:
|
||||
return await self.drafts.validate_draft_workspace(workspace_id=workspace_id)
|
||||
|
||||
async def compile_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self.drafts.compile_draft_workspace(workspace_id=workspace_id)
|
||||
|
||||
async def patch_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
@@ -362,23 +371,6 @@ class WorkflowApi:
|
||||
merge=merge,
|
||||
)
|
||||
|
||||
async def add_state_schema_from_output(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
output_field: str,
|
||||
state_path: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self.drafts.add_state_schema_from_output(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
output_field=output_field,
|
||||
state_path=state_path,
|
||||
)
|
||||
|
||||
async def bind_output_to_state(
|
||||
self,
|
||||
*,
|
||||
@@ -388,7 +380,7 @@ class WorkflowApi:
|
||||
output_field: str,
|
||||
state_path: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self.drafts.bind_output_to_state(
|
||||
return await self.draft_authoring.bind_output_to_state(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
@@ -405,24 +397,58 @@ class WorkflowApi:
|
||||
capability_name: str,
|
||||
route_from_step: str | None = None,
|
||||
route_from_outcome: str = "ok",
|
||||
route_outcome: str = "ok",
|
||||
route_to: str = "__end__",
|
||||
routes: dict[str, str] | None = None,
|
||||
input_map: dict[str, str] | None = None,
|
||||
bind_outputs: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return await self.drafts.add_step_from_capability(
|
||||
return await self.draft_authoring.add_step_from_capability(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
capability_name=capability_name,
|
||||
route_from_step=route_from_step,
|
||||
route_from_outcome=route_from_outcome,
|
||||
route_outcome=route_outcome,
|
||||
route_to=route_to,
|
||||
routes=routes,
|
||||
input_map=input_map,
|
||||
bind_outputs=bind_outputs,
|
||||
)
|
||||
|
||||
async def branch_draft(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
routes: dict[str, str],
|
||||
) -> dict[str, Any]:
|
||||
return await self.draft_authoring.branch_draft(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
routes=routes,
|
||||
)
|
||||
|
||||
async def handle_draft(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
branches: list[dict[str, str]],
|
||||
target: str,
|
||||
) -> dict[str, Any]:
|
||||
from .draft_authoring import DraftOutcomeRef
|
||||
|
||||
refs = [
|
||||
DraftOutcomeRef(step_id=b["step_id"], outcome=b["outcome"])
|
||||
for b in branches
|
||||
]
|
||||
return await self.draft_authoring.handle_draft(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
branches=refs,
|
||||
target=target,
|
||||
)
|
||||
|
||||
async def create_minimal_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
@@ -439,7 +465,7 @@ class WorkflowApi:
|
||||
error_message_source: Any | None = None,
|
||||
title: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return await self.drafts.create_minimal_draft_workspace(
|
||||
return await self.draft_authoring.create_minimal_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
name=name,
|
||||
capability_name=capability_name,
|
||||
|
||||
+25
-12
@@ -114,16 +114,6 @@ class WorkflowDraftSurface(Protocol):
|
||||
merge: bool = False,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def add_state_schema_from_output(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
output_field: str,
|
||||
state_path: str,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def bind_output_to_state(
|
||||
self,
|
||||
*,
|
||||
@@ -143,18 +133,41 @@ class WorkflowDraftSurface(Protocol):
|
||||
capability_name: str,
|
||||
route_from_step: str | None = None,
|
||||
route_from_outcome: str = "ok",
|
||||
route_outcome: str = "ok",
|
||||
route_to: str = "__end__",
|
||||
routes: dict[str, str] | None = None,
|
||||
input_map: dict[str, str] | None = None,
|
||||
bind_outputs: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def branch_draft(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
routes: dict[str, str],
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def handle_draft(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
branches: list[dict[str, str]],
|
||||
target: str,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def validate_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def compile_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def delete_draft_workspace(
|
||||
self,
|
||||
*,
|
||||
|
||||
+116
-51
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Literal
|
||||
|
||||
@@ -277,47 +278,6 @@ def set_step_output_map(
|
||||
)
|
||||
|
||||
|
||||
@app.command("add-state-from-output")
|
||||
def add_state_from_output(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
revision: Annotated[
|
||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||
],
|
||||
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||
output_field: Annotated[
|
||||
str,
|
||||
typer.Option("--output", help="Top-level capability output field."),
|
||||
],
|
||||
state_path: Annotated[
|
||||
str,
|
||||
typer.Option("--state", help="Root state path, for example state.after."),
|
||||
],
|
||||
) -> None:
|
||||
"""Copy one capability output field schema into draft state_schema.
|
||||
|
||||
Use this before mapping a step output into a new state field. The command
|
||||
reads the selected draft step's capability output schema, copies the
|
||||
requested output property schema, and preserves local $defs/definitions so
|
||||
JSON Schema refs remain valid.
|
||||
|
||||
Run `wf draft validate <workspace_id>` after adding state schema fields.
|
||||
"""
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.add_state_schema_from_output(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
output_field=output_field,
|
||||
state_path=state_path,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("bind-output-to-state")
|
||||
def bind_output_to_state(
|
||||
ctx: typer.Context,
|
||||
@@ -381,14 +341,13 @@ def add_step_from_capability(
|
||||
str,
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step."),
|
||||
] = "ok",
|
||||
route_outcome: Annotated[
|
||||
str,
|
||||
typer.Option("--outcome", help="Outcome emitted by the new step."),
|
||||
] = "ok",
|
||||
route_to: Annotated[
|
||||
str,
|
||||
typer.Option("--to", help="Target step id or __end__ for the new step."),
|
||||
] = "__end__",
|
||||
route: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--route",
|
||||
help="Route mapping OUTCOME=TARGET. Repeat for multiple outcomes.",
|
||||
),
|
||||
] = None,
|
||||
input_mapping: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
@@ -414,6 +373,15 @@ def add_step_from_capability(
|
||||
"""
|
||||
input_map = _parse_map_flags(input_mapping)
|
||||
bind_outputs = _parse_map_flags(output_mapping)
|
||||
routes: dict[str, str] = {}
|
||||
if route:
|
||||
for r in route:
|
||||
key, _, value = r.partition("=")
|
||||
if not key or not value:
|
||||
raise typer.BadParameter(
|
||||
f"invalid route: {r!r} (expected OUTCOME=TARGET)"
|
||||
)
|
||||
routes[key] = value
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
@@ -425,8 +393,7 @@ def add_step_from_capability(
|
||||
capability_name=capability_name,
|
||||
route_from_step=route_from_step,
|
||||
route_from_outcome=route_from_outcome,
|
||||
route_outcome=route_outcome,
|
||||
route_to=route_to,
|
||||
routes=routes or None,
|
||||
input_map=input_map,
|
||||
bind_outputs=bind_outputs,
|
||||
),
|
||||
@@ -434,6 +401,86 @@ def add_step_from_capability(
|
||||
)
|
||||
|
||||
|
||||
@app.command("branch")
|
||||
def branch_draft(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
revision: Annotated[
|
||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||
],
|
||||
step: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||
route: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--route",
|
||||
help="Route mapping OUTCOME=TARGET. Repeat for multiple outcomes.",
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Branch multiple outcome routes on a single step atomically."""
|
||||
routes: dict[str, str] = {}
|
||||
if route:
|
||||
for r in route:
|
||||
key, _, value = r.partition("=")
|
||||
if not key or not value:
|
||||
raise typer.BadParameter(
|
||||
f"invalid route: {r!r} (expected OUTCOME=TARGET)"
|
||||
)
|
||||
routes[key] = value
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.branch_draft(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step,
|
||||
routes=routes,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("handle")
|
||||
def handle_draft(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
revision: Annotated[
|
||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||
],
|
||||
to: Annotated[str, typer.Option("--to", help="Target step id or __end__.")],
|
||||
branch: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--branch",
|
||||
help="Branch mapping STEP:OUTCOME. Repeat for multiple branches.",
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Set a common target for multiple step/outcome pairs atomically."""
|
||||
branches: list[dict[str, str]] = []
|
||||
if branch:
|
||||
for b in branch:
|
||||
parts = b.rsplit(":", 1)
|
||||
if len(parts) != 2 or not parts[0] or not parts[1]:
|
||||
raise typer.BadParameter(
|
||||
f"invalid branch: {b!r} (expected STEP:OUTCOME)"
|
||||
)
|
||||
branches.append({"step_id": parts[0], "outcome": parts[1]})
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.handle_draft(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
branches=branches,
|
||||
target=to,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("validate")
|
||||
def validate_draft(
|
||||
ctx: typer.Context,
|
||||
@@ -449,6 +496,24 @@ def validate_draft(
|
||||
)
|
||||
|
||||
|
||||
@app.command("compile")
|
||||
def compile_draft(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
) -> None:
|
||||
"""Compile a stored draft workspace without mutating it."""
|
||||
context = load_cli_context(ctx)
|
||||
result = run_cli_operation(
|
||||
context,
|
||||
context.handlers.compile_draft_workspace(workspace_id=workspace_id),
|
||||
)
|
||||
if "compiled_plan" in result:
|
||||
emit_json(result["compiled_plan"])
|
||||
return
|
||||
typer.echo(json.dumps(result, indent=2, sort_keys=True), err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("delete")
|
||||
def delete_draft(
|
||||
ctx: typer.Context,
|
||||
|
||||
@@ -187,6 +187,12 @@ class ValidateDraftWorkspaceRequest(BaseModel):
|
||||
workspace_id: WorkspaceId
|
||||
|
||||
|
||||
class CompileDraftWorkspaceRequest(BaseModel):
|
||||
"""Typed MCP request for compiling one workspace draft without mutation."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
|
||||
|
||||
class SetDraftNameRequest(BaseModel):
|
||||
"""Typed MCP request for changing the workflow draft name."""
|
||||
|
||||
@@ -237,20 +243,6 @@ class SetStepOutputMapRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class AddStateFromOutputRequest(BaseModel):
|
||||
"""Typed MCP request for declaring a state field from a step output schema."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
revision: int = Field(ge=1, description="Expected current workspace revision.")
|
||||
step_id: str = Field(description="Draft step id whose capability output is used.")
|
||||
output_field: str = Field(
|
||||
description="Top-level output field to copy, for example after."
|
||||
)
|
||||
state_path: str = Field(
|
||||
description="Root state path to declare, for example state.after."
|
||||
)
|
||||
|
||||
|
||||
class BindOutputToStateRequest(BaseModel):
|
||||
"""Typed MCP request for binding one step output to one root state field."""
|
||||
|
||||
@@ -280,13 +272,13 @@ class AddStepFromCapabilityRequest(BaseModel):
|
||||
default="ok",
|
||||
description="Outcome on route_from_step that should route to the new step.",
|
||||
)
|
||||
route_outcome: str = Field(
|
||||
default="ok",
|
||||
description="Outcome emitted by the new step.",
|
||||
)
|
||||
route_to: str = Field(
|
||||
default="__end__",
|
||||
description="Target step id or __end__ for the new step outcome.",
|
||||
routes: dict[str, str] | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Outcome to target route map for the new step. If omitted, a single "
|
||||
"declared outcome routes to __end__; multiple declared outcomes "
|
||||
"require explicit routes."
|
||||
),
|
||||
)
|
||||
input_map: dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
@@ -298,6 +290,31 @@ class AddStepFromCapabilityRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class BranchDraftRequest(BaseModel):
|
||||
"""Typed MCP request for branching routes on a draft step."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
revision: int = Field(ge=1, description="Expected workspace revision.")
|
||||
step_id: str = Field(description="Draft step id whose routes should be branched.")
|
||||
routes: dict[str, str] = Field(description="Outcome to target step map.")
|
||||
|
||||
|
||||
class HandleDraftBranchItem(BaseModel):
|
||||
"""Single branch item for handle_draft."""
|
||||
|
||||
step_id: str = Field(description="Draft step id.")
|
||||
outcome: str = Field(description="Outcome label.")
|
||||
|
||||
|
||||
class HandleDraftRequest(BaseModel):
|
||||
"""Typed MCP request for handling draft branches."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
revision: int = Field(ge=1, description="Expected workspace revision.")
|
||||
branches: list[HandleDraftBranchItem] = Field(description="Branch items to handle.")
|
||||
target: str = Field(description="Target step id or __end__.")
|
||||
|
||||
|
||||
class DeleteDraftWorkspaceRequest(BaseModel):
|
||||
"""Typed MCP request payload for deleting one draft workspace."""
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ from wf_mcp.broker.service import WfMcpService
|
||||
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||
|
||||
from .models import (
|
||||
AddStateFromOutputRequest,
|
||||
AddStepFromCapabilityRequest,
|
||||
BindOutputToStateRequest,
|
||||
BranchDraftRequest,
|
||||
CallCapabilityResult,
|
||||
CompileDraftWorkspaceRequest,
|
||||
CreateArtifactFromWorkspaceRequest,
|
||||
CreateDraftWorkspaceFromCapabilityRequest,
|
||||
CreateDraftWorkspaceFromCapabilityResult,
|
||||
@@ -26,6 +27,7 @@ from .models import (
|
||||
DeleteDraftWorkspaceResult,
|
||||
DraftWorkspaceListResult,
|
||||
DraftWorkspaceResult,
|
||||
HandleDraftRequest,
|
||||
PatchDraftWorkspaceRequest,
|
||||
RunDeploymentResult,
|
||||
SetDraftNameRequest,
|
||||
@@ -368,6 +370,22 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
||||
await handlers.validate_draft_workspace(workspace_id=request.workspace_id)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.compile_draft_workspace",
|
||||
title="Compile Draft Workspace",
|
||||
description=(
|
||||
"Compile a stored draft workspace without mutating it. Returns "
|
||||
"compiled_plan and required_capabilities when valid, or diagnostics "
|
||||
"when invalid."
|
||||
),
|
||||
)
|
||||
async def compile_draft_workspace(
|
||||
request: CompileDraftWorkspaceRequest,
|
||||
) -> dict[str, Any]:
|
||||
return await handlers.compile_draft_workspace(
|
||||
workspace_id=request.workspace_id,
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.set_draft_name",
|
||||
title="Set Draft Name",
|
||||
@@ -442,27 +460,6 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.add_state_from_output",
|
||||
title="Add State From Output",
|
||||
description=(
|
||||
"Declare one root state field by copying a draft step capability output "
|
||||
"field schema, including local $defs/definitions when present."
|
||||
),
|
||||
)
|
||||
async def add_state_from_output(
|
||||
request: AddStateFromOutputRequest,
|
||||
) -> DraftWorkspaceResult:
|
||||
return DraftWorkspaceResult.model_validate(
|
||||
await handlers.add_state_schema_from_output(
|
||||
workspace_id=request.workspace_id,
|
||||
revision=request.revision,
|
||||
step_id=request.step_id,
|
||||
output_field=request.output_field,
|
||||
state_path=request.state_path,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.bind_output_to_state",
|
||||
title="Bind Output To State",
|
||||
@@ -503,13 +500,48 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
||||
capability_name=request.capability_name,
|
||||
route_from_step=request.route_from_step,
|
||||
route_from_outcome=request.route_from_outcome,
|
||||
route_outcome=request.route_outcome,
|
||||
route_to=request.route_to,
|
||||
routes=request.routes,
|
||||
input_map=request.input_map,
|
||||
bind_outputs=request.bind_outputs,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.branch_draft",
|
||||
title="Branch Draft",
|
||||
description=(
|
||||
"Branch multiple outcome routes on a single draft step atomically. "
|
||||
"Provided outcomes are updated while unspecified outcomes are preserved."
|
||||
),
|
||||
)
|
||||
async def branch_draft(request: BranchDraftRequest) -> DraftWorkspaceResult:
|
||||
return DraftWorkspaceResult.model_validate(
|
||||
await handlers.branch_draft(
|
||||
workspace_id=request.workspace_id,
|
||||
revision=request.revision,
|
||||
step_id=request.step_id,
|
||||
routes=request.routes,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.handle_draft",
|
||||
title="Handle Draft",
|
||||
description=("Set a common target for multiple step/outcome pairs atomically."),
|
||||
)
|
||||
async def handle_draft(request: HandleDraftRequest) -> DraftWorkspaceResult:
|
||||
return DraftWorkspaceResult.model_validate(
|
||||
await handlers.handle_draft(
|
||||
workspace_id=request.workspace_id,
|
||||
revision=request.revision,
|
||||
branches=[
|
||||
{"step_id": b.step_id, "outcome": b.outcome}
|
||||
for b in request.branches
|
||||
],
|
||||
target=request.target,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.create_minimal_draft_workspace",
|
||||
title="Create Minimal Draft Workspace",
|
||||
|
||||
@@ -6,13 +6,17 @@ from .errors import WorkflowRpcError
|
||||
from .models import (
|
||||
AddStepFromCapabilityParams,
|
||||
AdminEmptyParams,
|
||||
BranchDraftParams,
|
||||
CallCapabilityParams,
|
||||
CompileDraftWorkspaceParams,
|
||||
CreateArtifactFromPlanParams,
|
||||
CreateArtifactFromWorkspaceParams,
|
||||
CreateDraftFromCapabilityParams,
|
||||
CreateWrapperFromWorkspaceParams,
|
||||
DeleteDeploymentParams,
|
||||
GetDraftWorkspaceParams,
|
||||
HandleDraftBranch,
|
||||
HandleDraftParams,
|
||||
HealthParams,
|
||||
InspectArtifactParams,
|
||||
InspectCapabilityParams,
|
||||
@@ -42,15 +46,19 @@ from .models import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CreateArtifactFromPlanParams",
|
||||
"CreateArtifactFromWorkspaceParams",
|
||||
"AddStepFromCapabilityParams",
|
||||
"AdminEmptyParams",
|
||||
"BranchDraftParams",
|
||||
"CallCapabilityParams",
|
||||
"CompileDraftWorkspaceParams",
|
||||
"CreateArtifactFromPlanParams",
|
||||
"CreateArtifactFromWorkspaceParams",
|
||||
"CreateDraftFromCapabilityParams",
|
||||
"CreateWrapperFromWorkspaceParams",
|
||||
"DeleteDeploymentParams",
|
||||
"GetDraftWorkspaceParams",
|
||||
"HandleDraftBranch",
|
||||
"HandleDraftParams",
|
||||
"HealthParams",
|
||||
"InspectArtifactParams",
|
||||
"InspectCapabilityParams",
|
||||
|
||||
@@ -141,26 +141,6 @@ class RpcDraftClientMixin:
|
||||
},
|
||||
)
|
||||
|
||||
async def add_state_schema_from_output(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
output_field: str,
|
||||
state_path: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.add_state_from_output",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"step_id": step_id,
|
||||
"output_field": output_field,
|
||||
"state_path": state_path,
|
||||
},
|
||||
)
|
||||
|
||||
async def bind_output_to_state(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
@@ -190,8 +170,7 @@ class RpcDraftClientMixin:
|
||||
capability_name: str,
|
||||
route_from_step: str | None = None,
|
||||
route_from_outcome: str = "ok",
|
||||
route_outcome: str = "ok",
|
||||
route_to: str = "__end__",
|
||||
routes: dict[str, str] | None = None,
|
||||
input_map: dict[str, str] | None = None,
|
||||
bind_outputs: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -204,13 +183,48 @@ class RpcDraftClientMixin:
|
||||
"capability_name": capability_name,
|
||||
"route_from_step": route_from_step,
|
||||
"route_from_outcome": route_from_outcome,
|
||||
"route_outcome": route_outcome,
|
||||
"route_to": route_to,
|
||||
"routes": routes,
|
||||
"input_map": input_map or {},
|
||||
"bind_outputs": bind_outputs or {},
|
||||
},
|
||||
)
|
||||
|
||||
async def branch_draft(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
routes: dict[str, str],
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.branch",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"step_id": step_id,
|
||||
"routes": routes,
|
||||
},
|
||||
)
|
||||
|
||||
async def handle_draft(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
branches: list[dict[str, str]],
|
||||
target: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.handle",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"branches": branches,
|
||||
"target": target,
|
||||
},
|
||||
)
|
||||
|
||||
async def validate_draft_workspace(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
@@ -221,6 +235,16 @@ class RpcDraftClientMixin:
|
||||
{"workspace_id": workspace_id},
|
||||
)
|
||||
|
||||
async def compile_draft_workspace(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.compile",
|
||||
{"workspace_id": workspace_id},
|
||||
)
|
||||
|
||||
async def delete_draft_workspace(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
|
||||
@@ -8,14 +8,16 @@ from wf_server import WorkflowServer
|
||||
|
||||
from ..errors import WorkflowRpcError, raise_workflow_rpc_error
|
||||
from ..models import (
|
||||
AddStateFromOutputParams,
|
||||
AddStepFromCapabilityParams,
|
||||
BindOutputToStateParams,
|
||||
BranchDraftParams,
|
||||
CompileDraftWorkspaceParams,
|
||||
CreateArtifactFromWorkspaceParams,
|
||||
CreateDraftFromCapabilityParams,
|
||||
CreateWrapperFromWorkspaceParams,
|
||||
DeleteDraftWorkspaceParams,
|
||||
GetDraftWorkspaceParams,
|
||||
HandleDraftParams,
|
||||
ListDraftWorkspacesParams,
|
||||
PatchDraftParams,
|
||||
PatchDraftWorkspaceParams,
|
||||
@@ -180,24 +182,6 @@ def register_methods(
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.add_state_from_output",
|
||||
errors=[WorkflowRpcError],
|
||||
)
|
||||
async def workflow_draft_workspaces_add_state_from_output(
|
||||
params: AddStateFromOutputParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.add_state_schema_from_output(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
step_id=params.step_id,
|
||||
output_field=params.output_field,
|
||||
state_path=params.state_path,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.bind_output_to_state",
|
||||
errors=[WorkflowRpcError],
|
||||
@@ -231,14 +215,48 @@ def register_methods(
|
||||
capability_name=params.capability_name,
|
||||
route_from_step=params.route_from_step,
|
||||
route_from_outcome=params.route_from_outcome,
|
||||
route_outcome=params.route_outcome,
|
||||
route_to=params.route_to,
|
||||
routes=params.routes,
|
||||
input_map=params.input_map,
|
||||
bind_outputs=params.bind_outputs,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.branch", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_branch(
|
||||
params: BranchDraftParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.branch_draft(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
step_id=params.step_id,
|
||||
routes=params.routes,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.handle", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_handle(
|
||||
params: HandleDraftParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.handle_draft(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
branches=[
|
||||
{"step_id": b.step_id, "outcome": b.outcome}
|
||||
for b in params.branches
|
||||
],
|
||||
target=params.target,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.validate", errors=[WorkflowRpcError]
|
||||
)
|
||||
@@ -252,6 +270,19 @@ def register_methods(
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.compile", errors=[WorkflowRpcError]
|
||||
)
|
||||
async def workflow_draft_workspaces_compile(
|
||||
params: CompileDraftWorkspaceParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.compile_draft_workspace(
|
||||
workspace_id=params.workspace_id,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.delete", errors=[WorkflowRpcError]
|
||||
)
|
||||
|
||||
@@ -141,14 +141,6 @@ class SetStepOutputMapParams(RpcParamsModel):
|
||||
merge: bool = False
|
||||
|
||||
|
||||
class AddStateFromOutputParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
step_id: str = Field(min_length=1)
|
||||
output_field: str = Field(min_length=1)
|
||||
state_path: str = Field(min_length=1)
|
||||
|
||||
|
||||
class BindOutputToStateParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
@@ -164,16 +156,38 @@ class AddStepFromCapabilityParams(RpcParamsModel):
|
||||
capability_name: str = Field(min_length=1)
|
||||
route_from_step: str | None = None
|
||||
route_from_outcome: str = Field(default="ok", min_length=1)
|
||||
route_outcome: str = Field(default="ok", min_length=1)
|
||||
route_to: str = Field(default="__end__", min_length=1)
|
||||
routes: dict[str, str] | None = None
|
||||
input_map: dict[str, str] = Field(default_factory=dict)
|
||||
bind_outputs: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class BranchDraftParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
step_id: str = Field(min_length=1)
|
||||
routes: dict[str, str]
|
||||
|
||||
|
||||
class HandleDraftBranch(RpcParamsModel):
|
||||
step_id: str = Field(min_length=1)
|
||||
outcome: str = Field(min_length=1)
|
||||
|
||||
|
||||
class HandleDraftParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
branches: list[HandleDraftBranch]
|
||||
target: str = Field(min_length=1)
|
||||
|
||||
|
||||
class ValidateDraftWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class CompileDraftWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
class DeleteDraftWorkspaceParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user