feat: add semantic draft authoring operations

This commit is contained in:
lda
2026-06-27 17:14:17 +07:00 Verified
parent 5cfe513efc
commit 60a3815452
27 changed files with 1383 additions and 741 deletions
+3 -1
View File
@@ -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,
+392
View File
@@ -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
+65
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
*,