workflow builder deprecated field stop use in wf_mcp
This commit is contained in:
@@ -143,6 +143,22 @@ Use these categories:
|
|||||||
|
|
||||||
Add a small checklist under this task before implementation. Do not blindly replace all strings.
|
Add a small checklist under this task before implementation. Do not blindly replace all strings.
|
||||||
|
|
||||||
|
Findings from the first inventory pass:
|
||||||
|
|
||||||
|
- `src/wf_artifacts/drafts/adapter.py` is the highest-value runtime hit: it still
|
||||||
|
calls `WorkflowBuilder.use_ref(..., in_map=..., input_values=..., out_map=...)`
|
||||||
|
and `WorkflowBuilder.use(..., out_map=...)`, causing deprecation warnings from
|
||||||
|
MCP draft/workspace tests. This should be changed to canonical binding lists.
|
||||||
|
- Raw workflow-plan tests in `tests/wf_mcp/test_service.py`,
|
||||||
|
`tests/wf_mcp/test_broker_server.py`, `tests/wf_mcp/test_workflow_surface.py`,
|
||||||
|
and `tests/artifacts/test_factory.py` intentionally exercise raw-plan
|
||||||
|
compatibility. Do not bulk-rewrite those while raw-plan escape hatches remain.
|
||||||
|
- Draft model tests still use `state_schema.fields` as compatibility input. That
|
||||||
|
can stay as parse input, but new docs/examples should prefer JSON Schema
|
||||||
|
`properties`.
|
||||||
|
- Docs already explain parse-only compatibility in several places, but older
|
||||||
|
operator/runbook examples still need canonical `input` / `output` examples.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 2: Draft Adapter Emits Canonical Builder Bindings
|
## Task 2: Draft Adapter Emits Canonical Builder Bindings
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ Concrete MCP sequence:
|
|||||||
2. `wf.workflow.call_capability` with a small payload to verify the selected
|
2. `wf.workflow.call_capability` with a small payload to verify the selected
|
||||||
capability behaves as expected.
|
capability behaves as expected.
|
||||||
3. `wf.workflow.create_minimal_draft_workspace` with a `request` object that
|
3. `wf.workflow.create_minimal_draft_workspace` with a `request` object that
|
||||||
contains schemas, `input_map`, and `output_map`.
|
contains schemas plus canonical `input` and `output` binding lists.
|
||||||
4. `wf.workflow.list_draft_workspaces` if the client needs to rediscover
|
4. `wf.workflow.list_draft_workspaces` if the client needs to rediscover
|
||||||
existing workspace ids.
|
existing workspace ids.
|
||||||
5. `wf.workflow.get_draft_workspace` with `include_draft=true` if the client
|
5. `wf.workflow.get_draft_workspace` with `include_draft=true` if the client
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ Workspace patches are optimistic-concurrency guarded. Pass the current
|
|||||||
|
|
||||||
Workspace mutation tools use a single `request` object in MCP Inspector. That
|
Workspace mutation tools use a single `request` object in MCP Inspector. That
|
||||||
keeps the form grouped and lets the schema describe fields like
|
keeps the form grouped and lets the schema describe fields like
|
||||||
`input_schema`, `output_map`, and `error_message_source`.
|
`input_schema`, canonical `output`, and `error_message_source`.
|
||||||
|
|
||||||
Use `create_wrapper_from_workspace` when the draft is meant to normalize a raw
|
Use `create_wrapper_from_workspace` when the draft is meant to normalize a raw
|
||||||
capability into a reusable workflow-facing wrapper. It is the same validation
|
capability into a reusable workflow-facing wrapper. It is the same validation
|
||||||
@@ -406,12 +406,18 @@ Minimal example:
|
|||||||
},
|
},
|
||||||
"required": ["echoed"]
|
"required": ["echoed"]
|
||||||
},
|
},
|
||||||
"input_map": {
|
"input": [
|
||||||
"input.text": "text"
|
{
|
||||||
},
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
"output_map": {
|
"path": {"root": "input", "parts": ["text"]}
|
||||||
"echoed": "state.echoed"
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -406,7 +406,8 @@ Patch calls must include the current `revision`; stale revisions return
|
|||||||
|
|
||||||
`create_minimal_draft_workspace` is intentionally only a bootstrapper. It wires
|
`create_minimal_draft_workspace` is intentionally only a bootstrapper. It wires
|
||||||
an `error` outcome for naive MCP wrappers only when `error_message_source` is
|
an `error` outcome for naive MCP wrappers only when `error_message_source` is
|
||||||
provided or a state path can be derived from `output_map`. Provider-specific
|
provided or a state path can be derived from canonical `output` bindings or the
|
||||||
|
compatibility `output_map`. Provider-specific
|
||||||
error envelopes still belong in saved wrapper artifacts or follow-up patches.
|
error envelopes still belong in saved wrapper artifacts or follow-up patches.
|
||||||
|
|
||||||
In MCP Inspector, workspace mutation tools accept a single `request` object.
|
In MCP Inspector, workspace mutation tools accept a single `request` object.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from wf_authoring import WorkflowBuilder
|
from wf_authoring import WorkflowBuilder
|
||||||
from wf_authoring.dsl import PathExpr
|
from wf_authoring.dsl import PathExpr
|
||||||
from wf_core import JoinNode, Workflow
|
from wf_core import JoinNode, Workflow
|
||||||
@@ -42,9 +44,8 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
|||||||
return builder.use_ref(
|
return builder.use_ref(
|
||||||
step.use,
|
step.use,
|
||||||
id=step_id,
|
id=step_id,
|
||||||
in_map=step.in_,
|
input=_draft_input_bindings(step),
|
||||||
input_values=step.with_,
|
output=_draft_output_bindings(step),
|
||||||
out_map=step.out,
|
|
||||||
desc=step.desc,
|
desc=step.desc,
|
||||||
)
|
)
|
||||||
if isinstance(step, DraftForeachStep):
|
if isinstance(step, DraftForeachStep):
|
||||||
@@ -88,3 +89,24 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
|||||||
default=step.match.default,
|
default=step.match.default,
|
||||||
).entry
|
).entry
|
||||||
raise TypeError(f"unsupported draft step {type(step)!r}")
|
raise TypeError(f"unsupported draft step {type(step)!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _draft_input_bindings(step: DraftUseStep) -> list[dict[str, Any]]:
|
||||||
|
"""Translate draft input maps into canonical core input binding structs.
|
||||||
|
|
||||||
|
Draft JSON keeps `in` and `with` because they are compact patch targets for
|
||||||
|
LLM clients. The compiled workflow should not re-emit deprecated builder map
|
||||||
|
sugar, so this adapter boundary converts them to `NodeUse.input`.
|
||||||
|
"""
|
||||||
|
literal_bindings = [
|
||||||
|
{"target": target, "value": value} for target, value in step.with_.items()
|
||||||
|
]
|
||||||
|
path_bindings = [
|
||||||
|
{"target": target, "path": source} for source, target in step.in_.items()
|
||||||
|
]
|
||||||
|
return [*literal_bindings, *path_bindings]
|
||||||
|
|
||||||
|
|
||||||
|
def _draft_output_bindings(step: DraftUseStep) -> list[dict[str, str]]:
|
||||||
|
"""Translate draft output maps into canonical core output binding structs."""
|
||||||
|
return [{"source": source, "target": target} for source, target in step.out.items()]
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ from wf_platform import (
|
|||||||
)
|
)
|
||||||
from wf_authoring import build_async_registry
|
from wf_authoring import build_async_registry
|
||||||
from wf_core import RuntimeContext
|
from wf_core import RuntimeContext
|
||||||
|
from wf_core.models.steps import (
|
||||||
|
InputBinding,
|
||||||
|
InputPathBinding,
|
||||||
|
InputValueBinding,
|
||||||
|
OutputBinding,
|
||||||
|
)
|
||||||
|
|
||||||
from ..events import make_event
|
from ..events import make_event
|
||||||
from ..models import RawWorkflowPlan
|
from ..models import RawWorkflowPlan
|
||||||
@@ -214,7 +220,8 @@ class WorkflowSurfaceHandlers:
|
|||||||
query=query,
|
query=query,
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
rows.append({
|
rows.append(
|
||||||
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"source_id": "workflow",
|
"source_id": "workflow",
|
||||||
"kind": "wrapper_artifact",
|
"kind": "wrapper_artifact",
|
||||||
@@ -226,7 +233,8 @@ class WorkflowSurfaceHandlers:
|
|||||||
"is_async": True,
|
"is_async": True,
|
||||||
"input_fields": _schema_field_names(artifact.input_schema),
|
"input_fields": _schema_field_names(artifact.input_schema),
|
||||||
"output_fields": _schema_field_names(artifact.output_schema),
|
"output_fields": _schema_field_names(artifact.output_schema),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def _wrapper_capability_detail(
|
def _wrapper_capability_detail(
|
||||||
@@ -445,10 +453,12 @@ class WorkflowSurfaceHandlers:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
required_sources = sorted({
|
required_sources = sorted(
|
||||||
|
{
|
||||||
capability.logical_source
|
capability.logical_source
|
||||||
for capability in workflow_artifact.required_capability_map().values()
|
for capability in workflow_artifact.required_capability_map().values()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"artifact_id": workflow_artifact.id,
|
"artifact_id": workflow_artifact.id,
|
||||||
"version": workflow_artifact.version,
|
"version": workflow_artifact.version,
|
||||||
@@ -628,26 +638,34 @@ class WorkflowSurfaceHandlers:
|
|||||||
input_schema: dict[str, Any],
|
input_schema: dict[str, Any],
|
||||||
state_schema: dict[str, Any],
|
state_schema: dict[str, Any],
|
||||||
output_schema: dict[str, Any],
|
output_schema: dict[str, Any],
|
||||||
input_map: dict[str, str],
|
input: Sequence[InputBinding] | None = None,
|
||||||
output_map: dict[str, str],
|
output: Sequence[OutputBinding] | None = None,
|
||||||
|
input_map: dict[str, str] | None = None,
|
||||||
|
output_map: dict[str, str] | None = None,
|
||||||
error_message_source: str | None = None,
|
error_message_source: str | None = None,
|
||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Bootstrap the smallest patchable draft around one workflow capability."""
|
"""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 (
|
outcomes = self._outcomes_for_capability(capability_name) or (
|
||||||
DEFAULT_OK_OUTCOME,
|
DEFAULT_OK_OUTCOME,
|
||||||
)
|
)
|
||||||
steps: dict[str, Any] = {
|
steps: dict[str, Any] = {
|
||||||
DEFAULT_CALL_STEP_ID: {
|
DEFAULT_CALL_STEP_ID: {
|
||||||
"use": capability_name,
|
"use": capability_name,
|
||||||
"in": input_map,
|
"in": draft_input,
|
||||||
"out": output_map,
|
"with": draft_with,
|
||||||
|
"out": draft_output,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
routes: dict[str, dict[str, str]] = {
|
routes: dict[str, dict[str, str]] = {
|
||||||
DEFAULT_CALL_STEP_ID: {DEFAULT_OK_OUTCOME: "__end__"}
|
DEFAULT_CALL_STEP_ID: {DEFAULT_OK_OUTCOME: "__end__"}
|
||||||
}
|
}
|
||||||
error_source = error_message_source or _first_state_path(output_map)
|
error_source = error_message_source or _first_state_path(draft_output)
|
||||||
if DEFAULT_ERROR_OUTCOME in outcomes and error_source is not None:
|
if DEFAULT_ERROR_OUTCOME in outcomes and error_source is not None:
|
||||||
# The bootstrapper cannot infer provider-specific error envelopes.
|
# The bootstrapper cannot infer provider-specific error envelopes.
|
||||||
# It only wires an error route when the caller gave, or output_map
|
# It only wires an error route when the caller gave, or output_map
|
||||||
@@ -684,6 +702,8 @@ class WorkflowSurfaceHandlers:
|
|||||||
input_schema: dict[str, Any] | None = None,
|
input_schema: dict[str, Any] | None = None,
|
||||||
state_schema: dict[str, Any] | None = None,
|
state_schema: dict[str, Any] | None = None,
|
||||||
output_schema: dict[str, Any] | None = None,
|
output_schema: dict[str, Any] | None = None,
|
||||||
|
input: Sequence[InputBinding] | None = None,
|
||||||
|
output: Sequence[OutputBinding] | None = None,
|
||||||
input_map: dict[str, str] | None = None,
|
input_map: dict[str, str] | None = None,
|
||||||
output_map: dict[str, str] | None = None,
|
output_map: dict[str, str] | None = None,
|
||||||
error_message_source: str | None = None,
|
error_message_source: str | None = None,
|
||||||
@@ -698,8 +718,12 @@ class WorkflowSurfaceHandlers:
|
|||||||
input_schema=input_schema or hints["input_schema"],
|
input_schema=input_schema or hints["input_schema"],
|
||||||
state_schema=state_schema or hints["state_schema"],
|
state_schema=state_schema or hints["state_schema"],
|
||||||
output_schema=output_schema or hints["output_schema"],
|
output_schema=output_schema or hints["output_schema"],
|
||||||
input_map=input_map or hints["input_map"],
|
input=input,
|
||||||
output_map=output_map or hints["output_map"],
|
output=output,
|
||||||
|
input_map=None if input is not None else (input_map or hints["input_map"]),
|
||||||
|
output_map=None
|
||||||
|
if output is not None
|
||||||
|
else (output_map or hints["output_map"]),
|
||||||
error_message_source=error_message_source,
|
error_message_source=error_message_source,
|
||||||
title=title,
|
title=title,
|
||||||
)
|
)
|
||||||
@@ -905,14 +929,16 @@ def _available_sources(service: WfMcpService) -> list[AvailableSource]:
|
|||||||
if (capability_name := _capability_name(spec.name)) is not None
|
if (capability_name := _capability_name(spec.name)) is not None
|
||||||
if (detail := node_spec_details.get(spec.name)) is not None
|
if (detail := node_spec_details.get(spec.name)) is not None
|
||||||
}
|
}
|
||||||
capabilities.update({
|
capabilities.update(
|
||||||
|
{
|
||||||
capability_name: AvailableCapability(
|
capability_name: AvailableCapability(
|
||||||
name=capability_name,
|
name=capability_name,
|
||||||
kind="reducer",
|
kind="reducer",
|
||||||
)
|
)
|
||||||
for reducer in source.capabilities.reducers.values()
|
for reducer in source.capabilities.reducers.values()
|
||||||
if (capability_name := _capability_name(reducer.name)) is not None
|
if (capability_name := _capability_name(reducer.name)) is not None
|
||||||
})
|
}
|
||||||
|
)
|
||||||
sources.append(
|
sources.append(
|
||||||
AvailableSource(
|
AvailableSource(
|
||||||
id=source.id,
|
id=source.id,
|
||||||
@@ -976,9 +1002,9 @@ def _observed_node_specs(service: WfMcpService) -> dict[str, NodeSpecInventory]:
|
|||||||
observed: dict[str, NodeSpecInventory] = {}
|
observed: dict[str, NodeSpecInventory] = {}
|
||||||
for source in service.capability_sources.values():
|
for source in service.capability_sources.values():
|
||||||
inventory = source.as_inventory()
|
inventory = source.as_inventory()
|
||||||
observed.update({
|
observed.update(
|
||||||
detail.name: detail for detail in inventory.capabilities.node_spec_details
|
{detail.name: detail for detail in inventory.capabilities.node_spec_details}
|
||||||
})
|
)
|
||||||
return observed
|
return observed
|
||||||
|
|
||||||
|
|
||||||
@@ -1013,6 +1039,47 @@ def _first_state_path(output_map: dict[str, str]) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _draft_input_maps(
|
||||||
|
*,
|
||||||
|
input: Sequence[InputBinding] | None,
|
||||||
|
input_map: dict[str, str] | None,
|
||||||
|
) -> tuple[dict[str, str], dict[str, Any]]:
|
||||||
|
"""Convert canonical MCP input bindings into draft `in` and `with` maps.
|
||||||
|
|
||||||
|
Draft workspaces intentionally keep compact maps as patch targets, while
|
||||||
|
MCP-facing request models prefer the canonical core binding structs. This
|
||||||
|
helper keeps that translation explicit at the frontend boundary.
|
||||||
|
"""
|
||||||
|
if input is not None and input_map is not None:
|
||||||
|
raise ValueError("cannot mix canonical input bindings with input_map")
|
||||||
|
if input is None:
|
||||||
|
return dict(input_map or {}), {}
|
||||||
|
|
||||||
|
mapped_inputs: dict[str, str] = {}
|
||||||
|
literal_inputs: dict[str, Any] = {}
|
||||||
|
for binding in input:
|
||||||
|
if isinstance(binding, InputPathBinding):
|
||||||
|
mapped_inputs[str(binding.path)] = str(binding.target)
|
||||||
|
elif isinstance(binding, InputValueBinding):
|
||||||
|
literal_inputs[str(binding.target)] = binding.value
|
||||||
|
else: # pragma: no cover - defensive against future input binding variants.
|
||||||
|
raise TypeError(f"unsupported input binding {binding!r}")
|
||||||
|
return mapped_inputs, literal_inputs
|
||||||
|
|
||||||
|
|
||||||
|
def _draft_output_map(
|
||||||
|
*,
|
||||||
|
output: Sequence[OutputBinding] | None,
|
||||||
|
output_map: dict[str, str] | None,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Convert canonical MCP output bindings into the draft `out` map."""
|
||||||
|
if output is not None and output_map is not None:
|
||||||
|
raise ValueError("cannot mix canonical output bindings with output_map")
|
||||||
|
if output is None:
|
||||||
|
return dict(output_map or {})
|
||||||
|
return {str(binding.source): str(binding.target) for binding in output}
|
||||||
|
|
||||||
|
|
||||||
def _escape_json_pointer(value: str) -> str:
|
def _escape_json_pointer(value: str) -> str:
|
||||||
"""Escape one JSON Pointer path segment for generated JSON Patch helpers."""
|
"""Escape one JSON Pointer path segment for generated JSON Patch helpers."""
|
||||||
return value.replace("~", "~0").replace("/", "~1")
|
return value.replace("~", "~0").replace("/", "~1")
|
||||||
@@ -1057,7 +1124,8 @@ def _artifact_capability_id(artifact: WorkflowArtifact) -> str:
|
|||||||
|
|
||||||
def _raw_plan_from_artifact(artifact: WorkflowArtifact) -> RawWorkflowPlan:
|
def _raw_plan_from_artifact(artifact: WorkflowArtifact) -> RawWorkflowPlan:
|
||||||
"""Validate the stored plan shape expected by the broker workflow runner."""
|
"""Validate the stored plan shape expected by the broker workflow runner."""
|
||||||
return RawWorkflowPlan.model_validate({
|
return RawWorkflowPlan.model_validate(
|
||||||
|
{
|
||||||
"name": _plan_field(artifact, "name"),
|
"name": _plan_field(artifact, "name"),
|
||||||
"input_schema": _plan_field(artifact, "input_schema"),
|
"input_schema": _plan_field(artifact, "input_schema"),
|
||||||
"state_schema": _plan_field(artifact, "state_schema"),
|
"state_schema": _plan_field(artifact, "state_schema"),
|
||||||
@@ -1065,7 +1133,8 @@ def _raw_plan_from_artifact(artifact: WorkflowArtifact) -> RawWorkflowPlan:
|
|||||||
"start": _plan_field(artifact, "start"),
|
"start": _plan_field(artifact, "start"),
|
||||||
"nodes": _plan_field(artifact, "nodes"),
|
"nodes": _plan_field(artifact, "nodes"),
|
||||||
"edges": _plan_field(artifact, "edges"),
|
"edges": _plan_field(artifact, "edges"),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _plan_field(artifact: WorkflowArtifact, field_name: str) -> Any:
|
def _plan_field(artifact: WorkflowArtifact, field_name: str) -> Any:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from wf_artifacts import ArtifactKind
|
from wf_artifacts import ArtifactKind
|
||||||
from wf_artifacts.draft_workspaces.models import WORKSPACE_ID_PATTERN
|
from wf_artifacts.draft_workspaces.models import WORKSPACE_ID_PATTERN
|
||||||
|
from wf_core.models.steps import InputBinding, OutputBinding
|
||||||
|
|
||||||
WorkspaceId = Annotated[
|
WorkspaceId = Annotated[
|
||||||
str,
|
str,
|
||||||
@@ -27,8 +28,29 @@ DraftPathMap = Annotated[
|
|||||||
dict[str, str],
|
dict[str, str],
|
||||||
Field(
|
Field(
|
||||||
description=(
|
description=(
|
||||||
"Map local draft paths to workflow paths, for example "
|
"Compatibility map form for draft paths. Prefer canonical input/output "
|
||||||
"{'input.text': 'text'} or {'echoed': 'state.echoed'}."
|
"binding lists for new MCP/JSON clients."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
DraftInputBindings = Annotated[
|
||||||
|
list[InputBinding],
|
||||||
|
Field(
|
||||||
|
description=(
|
||||||
|
"Canonical node input bindings. Use path bindings such as "
|
||||||
|
"{'target': {'root': 'local', 'parts': ['text']}, "
|
||||||
|
"'path': {'root': 'input', 'parts': ['text']}} or value bindings "
|
||||||
|
"with {'target': ..., 'value': ...}."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
DraftOutputBindings = Annotated[
|
||||||
|
list[OutputBinding],
|
||||||
|
Field(
|
||||||
|
description=(
|
||||||
|
"Canonical node output bindings. Example: {'source': {'root': "
|
||||||
|
"'local', 'parts': ['echoed']}, 'target': {'root': 'state', "
|
||||||
|
"'parts': ['echoed']}}."
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -203,25 +225,40 @@ class CreateMinimalDraftWorkspaceRequest(BaseModel):
|
|||||||
)
|
)
|
||||||
state_schema: JsonSchemaObject = Field(
|
state_schema: JsonSchemaObject = Field(
|
||||||
description=(
|
description=(
|
||||||
"Workflow state schema. The current core state schema uses a fields "
|
"Workflow state JSON Schema. Prefer object properties with reducer "
|
||||||
"object; keep it small and explicit."
|
"extension keywords; legacy fields input is compatibility-only."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
output_schema: JsonSchemaObject = Field(
|
output_schema: JsonSchemaObject = Field(
|
||||||
description="Public output JSON Schema for the workflow or wrapper being drafted."
|
description="Public output JSON Schema for the workflow or wrapper being drafted."
|
||||||
)
|
)
|
||||||
input_map: DraftPathMap = Field(
|
input: DraftInputBindings | None = Field(
|
||||||
|
default=None,
|
||||||
description=(
|
description=(
|
||||||
"Map public workflow input paths to local capability input paths. "
|
"Preferred canonical input bindings for the called capability. "
|
||||||
"Example: {'input.text': 'message'} sends workflow input.text to "
|
"Use this instead of input_map for new clients."
|
||||||
"capability field message."
|
),
|
||||||
)
|
)
|
||||||
)
|
output: DraftOutputBindings | None = Field(
|
||||||
output_map: DraftPathMap = Field(
|
default=None,
|
||||||
description=(
|
description=(
|
||||||
"Map local capability output paths to workflow state paths. Example: "
|
"Preferred canonical output bindings for writes into workflow state. "
|
||||||
"{'echoed': 'state.echoed'} stores capability output echoed in state.echoed."
|
"Use this instead of output_map for new clients."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
input_map: DraftPathMap | None = Field(
|
||||||
|
default=None,
|
||||||
|
description=(
|
||||||
|
"Deprecated compatibility map from workflow paths to local capability "
|
||||||
|
"input paths, for example {'input.text': 'message'}."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
output_map: DraftPathMap | None = Field(
|
||||||
|
default=None,
|
||||||
|
description=(
|
||||||
|
"Deprecated compatibility map from local capability output paths to "
|
||||||
|
"workflow state paths, for example {'echoed': 'state.echoed'}."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
error_message_source: str | None = Field(
|
error_message_source: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
@@ -260,13 +297,23 @@ class CreateDraftWorkspaceFromCapabilityRequest(BaseModel):
|
|||||||
default=None,
|
default=None,
|
||||||
description="Optional override for the hinted public output schema.",
|
description="Optional override for the hinted public output schema.",
|
||||||
)
|
)
|
||||||
|
input: DraftInputBindings | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Optional canonical override for the hinted workflow input bindings.",
|
||||||
|
)
|
||||||
|
output: DraftOutputBindings | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Optional canonical override for the hinted capability output bindings.",
|
||||||
|
)
|
||||||
input_map: DraftPathMap | None = Field(
|
input_map: DraftPathMap | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Optional override for the hinted workflow input map.",
|
description="Deprecated compatibility override for the hinted workflow input map.",
|
||||||
)
|
)
|
||||||
output_map: DraftPathMap | None = Field(
|
output_map: DraftPathMap | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Optional override for the hinted capability output map.",
|
description=(
|
||||||
|
"Deprecated compatibility override for the hinted capability output map."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
error_message_source: str | None = Field(
|
error_message_source: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
|||||||
@@ -340,7 +340,10 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.set_step_input_map",
|
name="wf.workflow.set_step_input_map",
|
||||||
title="Set Step Input Map",
|
title="Set Step Input Map",
|
||||||
description="Replace one step input map in a draft workspace.",
|
description=(
|
||||||
|
"Replace one compatibility step input map in a draft workspace. "
|
||||||
|
"New one-capability bootstraps should prefer canonical input bindings."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def set_step_input_map(
|
async def set_step_input_map(
|
||||||
request: SetStepInputMapRequest,
|
request: SetStepInputMapRequest,
|
||||||
@@ -357,7 +360,10 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.set_step_output_map",
|
name="wf.workflow.set_step_output_map",
|
||||||
title="Set Step Output Map",
|
title="Set Step Output Map",
|
||||||
description="Replace one step output map in a draft workspace.",
|
description=(
|
||||||
|
"Replace one compatibility step output map in a draft workspace. "
|
||||||
|
"New one-capability bootstraps should prefer canonical output bindings."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def set_step_output_map(
|
async def set_step_output_map(
|
||||||
request: SetStepOutputMapRequest,
|
request: SetStepOutputMapRequest,
|
||||||
@@ -376,7 +382,8 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
title="Create Minimal Draft Workspace",
|
title="Create Minimal Draft Workspace",
|
||||||
description=(
|
description=(
|
||||||
"Bootstrap a patchable draft workspace around one inspected capability. "
|
"Bootstrap a patchable draft workspace around one inspected capability. "
|
||||||
"Use this before patch helpers when authoring from MCP clients."
|
"Use canonical input/output binding lists for new MCP clients; "
|
||||||
|
"input_map/output_map remain compatibility fields."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def create_minimal_draft_workspace(
|
async def create_minimal_draft_workspace(
|
||||||
@@ -390,6 +397,8 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
input_schema=request.input_schema,
|
input_schema=request.input_schema,
|
||||||
state_schema=request.state_schema,
|
state_schema=request.state_schema,
|
||||||
output_schema=request.output_schema,
|
output_schema=request.output_schema,
|
||||||
|
input=request.input,
|
||||||
|
output=request.output,
|
||||||
input_map=request.input_map,
|
input_map=request.input_map,
|
||||||
output_map=request.output_map,
|
output_map=request.output_map,
|
||||||
error_message_source=request.error_message_source,
|
error_message_source=request.error_message_source,
|
||||||
@@ -417,6 +426,8 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
input_schema=request.input_schema,
|
input_schema=request.input_schema,
|
||||||
state_schema=request.state_schema,
|
state_schema=request.state_schema,
|
||||||
output_schema=request.output_schema,
|
output_schema=request.output_schema,
|
||||||
|
input=request.input,
|
||||||
|
output=request.output,
|
||||||
input_map=request.input_map,
|
input_map=request.input_map,
|
||||||
output_map=request.output_map,
|
output_map=request.output_map,
|
||||||
error_message_source=request.error_message_source,
|
error_message_source=request.error_message_source,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pydantic import BaseModel
|
|||||||
from wf_authoring import NodeSpec
|
from wf_authoring import NodeSpec
|
||||||
from wf_core import ReducerSpec
|
from wf_core import ReducerSpec
|
||||||
from wf_core.runtime.ops.merges import ReducerDefinition
|
from wf_core.runtime.ops.merges import ReducerDefinition
|
||||||
|
from wf_platform.refs import CapabilityRef
|
||||||
|
|
||||||
SourceKind = Literal["system", "connection"]
|
SourceKind = Literal["system", "connection"]
|
||||||
JsonObject = dict[str, Any]
|
JsonObject = dict[str, Any]
|
||||||
@@ -59,6 +60,7 @@ class ReducerInventory(BaseModel):
|
|||||||
"""Serializable public contract for one pure reducer."""
|
"""Serializable public contract for one pure reducer."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
|
ref: CapabilityRef
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
config_schema: JsonObject
|
config_schema: JsonObject
|
||||||
|
|
||||||
@@ -200,6 +202,7 @@ class CapabilitySource:
|
|||||||
reducer_details=tuple(
|
reducer_details=tuple(
|
||||||
ReducerInventory(
|
ReducerInventory(
|
||||||
name=reducer.name,
|
name=reducer.name,
|
||||||
|
ref=CapabilityRef.parse(reducer.name),
|
||||||
description=reducer.description,
|
description=reducer.description,
|
||||||
config_schema=reducer.config_schema,
|
config_schema=reducer.config_schema,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ from wf_core.models.steps import InputValueBinding
|
|||||||
|
|
||||||
|
|
||||||
def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
|
def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
|
||||||
draft = WorkflowDraft.model_validate({
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
"name": "echo",
|
"name": "echo",
|
||||||
"input_schema": {},
|
"input_schema": {},
|
||||||
"state_schema": {"fields": {}},
|
"state_schema": {"fields": {}},
|
||||||
@@ -18,7 +19,8 @@ def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
|
|||||||
"start": "echo",
|
"start": "echo",
|
||||||
"steps": {"echo": {"use": "demo.echo"}},
|
"steps": {"echo": {"use": "demo.echo"}},
|
||||||
"routes": {"echo": {"ok": "__end__"}},
|
"routes": {"echo": {"ok": "__end__"}},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
workflow = build_workflow_from_draft(draft)
|
workflow = build_workflow_from_draft(draft)
|
||||||
|
|
||||||
@@ -31,8 +33,41 @@ def test_adapter_lowers_keyed_use_steps_and_routes_through_builder() -> None:
|
|||||||
assert workflow.edges[0].to == "__end__"
|
assert workflow.edges[0].to == "__end__"
|
||||||
|
|
||||||
|
|
||||||
|
def test_adapter_lowers_use_steps_to_canonical_bindings() -> None:
|
||||||
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
|
"name": "echo",
|
||||||
|
"input_schema": {},
|
||||||
|
"state_schema": {"fields": {"echoed": {"type": "string"}}},
|
||||||
|
"output_schema": {},
|
||||||
|
"start": "echo",
|
||||||
|
"steps": {
|
||||||
|
"echo": {
|
||||||
|
"use": "demo.echo",
|
||||||
|
"in": {"input.text": "text"},
|
||||||
|
"out": {"echoed": "state.echoed"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": {"echo": {"ok": "__end__"}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow = build_workflow_from_draft(draft)
|
||||||
|
node = workflow.nodes[0]
|
||||||
|
|
||||||
|
assert isinstance(node, NodeUse)
|
||||||
|
dumped = node.model_dump(mode="json")
|
||||||
|
assert "in_map" not in dumped
|
||||||
|
assert "out_map" not in dumped
|
||||||
|
assert dumped["input"][0]["target"] == {"root": "local", "parts": ["text"]}
|
||||||
|
assert dumped["input"][0]["path"] == {"root": "input", "parts": ["text"]}
|
||||||
|
assert dumped["output"][0]["source"] == {"root": "local", "parts": ["echoed"]}
|
||||||
|
assert dumped["output"][0]["target"] == {"root": "state", "parts": ["echoed"]}
|
||||||
|
|
||||||
|
|
||||||
def test_adapter_lowers_static_inputs_for_constant_like_steps() -> None:
|
def test_adapter_lowers_static_inputs_for_constant_like_steps() -> None:
|
||||||
draft = WorkflowDraft.model_validate({
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
"name": "constant",
|
"name": "constant",
|
||||||
"input_schema": {},
|
"input_schema": {},
|
||||||
"state_schema": {"fields": {"message": {"type": "string"}}},
|
"state_schema": {"fields": {"message": {"type": "string"}}},
|
||||||
@@ -46,7 +81,8 @@ def test_adapter_lowers_static_inputs_for_constant_like_steps() -> None:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {"constant": {"ok": "__end__"}},
|
"routes": {"constant": {"ok": "__end__"}},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
workflow = build_workflow_from_draft(draft)
|
workflow = build_workflow_from_draft(draft)
|
||||||
node = workflow.nodes[0]
|
node = workflow.nodes[0]
|
||||||
@@ -88,7 +124,8 @@ def test_invalid_literal_input_map_does_not_fall_through_to_join() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_adapter_lowers_when_step_through_builder() -> None:
|
def test_adapter_lowers_when_step_through_builder() -> None:
|
||||||
draft = WorkflowDraft.model_validate({
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
"name": "when_example",
|
"name": "when_example",
|
||||||
"input_schema": {},
|
"input_schema": {},
|
||||||
"state_schema": {"fields": {}},
|
"state_schema": {"fields": {}},
|
||||||
@@ -109,7 +146,8 @@ def test_adapter_lowers_when_step_through_builder() -> None:
|
|||||||
"echo": {"use": "demo.echo"},
|
"echo": {"use": "demo.echo"},
|
||||||
},
|
},
|
||||||
"routes": {"echo": {"ok": "__end__"}},
|
"routes": {"echo": {"ok": "__end__"}},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
workflow = build_workflow_from_draft(draft)
|
workflow = build_workflow_from_draft(draft)
|
||||||
condition = workflow.nodes[0]
|
condition = workflow.nodes[0]
|
||||||
@@ -124,7 +162,8 @@ def test_adapter_lowers_when_step_through_builder() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_adapter_lowers_choose_step_through_builder() -> None:
|
def test_adapter_lowers_choose_step_through_builder() -> None:
|
||||||
draft = WorkflowDraft.model_validate({
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
"name": "choose_example",
|
"name": "choose_example",
|
||||||
"input_schema": {},
|
"input_schema": {},
|
||||||
"state_schema": {"fields": {}},
|
"state_schema": {"fields": {}},
|
||||||
@@ -160,7 +199,8 @@ def test_adapter_lowers_choose_step_through_builder() -> None:
|
|||||||
"high": {"ok": "__end__"},
|
"high": {"ok": "__end__"},
|
||||||
"fallback": {"ok": "__end__"},
|
"fallback": {"ok": "__end__"},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
workflow = build_workflow_from_draft(draft)
|
workflow = build_workflow_from_draft(draft)
|
||||||
condition_ids = [
|
condition_ids = [
|
||||||
@@ -178,7 +218,8 @@ def test_adapter_lowers_choose_step_through_builder() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_adapter_lowers_match_step_through_builder() -> None:
|
def test_adapter_lowers_match_step_through_builder() -> None:
|
||||||
draft = WorkflowDraft.model_validate({
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
"name": "match_example",
|
"name": "match_example",
|
||||||
"input_schema": {},
|
"input_schema": {},
|
||||||
"state_schema": {"fields": {}},
|
"state_schema": {"fields": {}},
|
||||||
@@ -202,7 +243,8 @@ def test_adapter_lowers_match_step_through_builder() -> None:
|
|||||||
"ready": {"ok": "__end__"},
|
"ready": {"ok": "__end__"},
|
||||||
"waiting": {"ok": "__end__"},
|
"waiting": {"ok": "__end__"},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
workflow = build_workflow_from_draft(draft)
|
workflow = build_workflow_from_draft(draft)
|
||||||
condition_ids = [
|
condition_ids = [
|
||||||
|
|||||||
@@ -64,5 +64,10 @@ def test_source_inventory_exposes_serializable_reducer_details() -> None:
|
|||||||
|
|
||||||
assert isinstance(detail, ReducerInventory)
|
assert isinstance(detail, ReducerInventory)
|
||||||
assert detail.name == "wf.std.max"
|
assert detail.name == "wf.std.max"
|
||||||
|
assert str(detail.ref.source) == "wf.std"
|
||||||
|
assert detail.ref.name == "max"
|
||||||
assert detail.description == "Keep the greater value."
|
assert detail.description == "Keep the greater value."
|
||||||
assert detail.config_schema == {"type": "object", "properties": {}}
|
assert detail.config_schema == {"type": "object", "properties": {}}
|
||||||
|
assert source.as_inventory().model_dump(mode="json")["capabilities"][
|
||||||
|
"reducer_details"
|
||||||
|
][0]["ref"] == {"source": "wf.std", "capability_key": "max"}
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
minimal_request = minimal_workspace_input["properties"]["request"]
|
minimal_request = minimal_workspace_input["properties"]["request"]
|
||||||
assert minimal_request["properties"]["workspace_id"]["pattern"]
|
assert minimal_request["properties"]["workspace_id"]["pattern"]
|
||||||
assert "error_message_source" in minimal_request["properties"]
|
assert "error_message_source" in minimal_request["properties"]
|
||||||
|
assert "input" in minimal_request["properties"]
|
||||||
|
assert "output" in minimal_request["properties"]
|
||||||
|
assert "input_map" in minimal_request["properties"]
|
||||||
assert (
|
assert (
|
||||||
minimal_request["properties"]["input_schema"]["description"]
|
minimal_request["properties"]["input_schema"]["description"]
|
||||||
== "Public input JSON Schema for the workflow or wrapper being "
|
== "Public input JSON Schema for the workflow or wrapper being "
|
||||||
@@ -149,6 +152,8 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
from_capability_request = from_capability_input["properties"]["request"]
|
from_capability_request = from_capability_input["properties"]["request"]
|
||||||
assert "capability_name" in from_capability_request["properties"]
|
assert "capability_name" in from_capability_request["properties"]
|
||||||
assert "input_schema" in from_capability_request["properties"]
|
assert "input_schema" in from_capability_request["properties"]
|
||||||
|
assert "input" in from_capability_request["properties"]
|
||||||
|
assert "output" in from_capability_request["properties"]
|
||||||
assert "output_map" in from_capability_request["properties"]
|
assert "output_map" in from_capability_request["properties"]
|
||||||
from_capability_output = tools_by_name[
|
from_capability_output = tools_by_name[
|
||||||
"wf.workflow.create_draft_workspace_from_capability"
|
"wf.workflow.create_draft_workspace_from_capability"
|
||||||
@@ -526,7 +531,8 @@ def test_server_reload_syncs_service_connection_source_enabled_state() -> None:
|
|||||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
tmp_path.mkdir(parents=True, exist_ok=True)
|
||||||
config_path = tmp_path / "wf_mcp.config.json"
|
config_path = tmp_path / "wf_mcp.config.json"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
json.dumps({
|
json.dumps(
|
||||||
|
{
|
||||||
"store_root": ".wf_mcp_store",
|
"store_root": ".wf_mcp_store",
|
||||||
"connections": [
|
"connections": [
|
||||||
{
|
{
|
||||||
@@ -541,7 +547,8 @@ def test_server_reload_syncs_service_connection_source_enabled_state() -> None:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
config = load_broker_config(config_path)
|
config = load_broker_config(config_path)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from wf_mcp.broker import WfMcpService
|
|||||||
from wf_mcp.models import ConnectionConfig, RawWorkflowPlan
|
from wf_mcp.models import ConnectionConfig, RawWorkflowPlan
|
||||||
from wf_mcp.storage import FileStore
|
from wf_mcp.storage import FileStore
|
||||||
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
|
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
|
||||||
|
from wf_core.models.steps import InputPathBinding, OutputBinding
|
||||||
|
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
|
||||||
from wf_platform import (
|
from wf_platform import (
|
||||||
CapabilityBuckets,
|
CapabilityBuckets,
|
||||||
CapabilitySource,
|
CapabilitySource,
|
||||||
@@ -680,6 +682,45 @@ def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() ->
|
|||||||
assert workspace.draft["steps"]["tool_error"]["in"] == {"state.echoed": "message"}
|
assert workspace.draft["steps"]["tool_error"]["in"] == {"state.echoed": "message"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace() -> None:
|
||||||
|
service = WfMcpService(
|
||||||
|
store=FileStore(local_temp_root() / "surface_minimal_canonical_mcp"),
|
||||||
|
artifact_store=FileWorkflowArtifactStore(
|
||||||
|
local_temp_root() / "surface_minimal_canonical"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
handlers = WorkflowSurfaceHandlers(service)
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
handlers.create_minimal_draft_workspace(
|
||||||
|
workspace_id="echo_draft",
|
||||||
|
name="echo",
|
||||||
|
capability_name="demo.personal.echo_tool",
|
||||||
|
input_schema={"type": "object"},
|
||||||
|
state_schema={"fields": {"echoed": {"type": "string"}}},
|
||||||
|
output_schema={"type": "object"},
|
||||||
|
input=[
|
||||||
|
InputPathBinding(
|
||||||
|
target=LocalPath(("text",)),
|
||||||
|
path=GraphSourcePath("input", ("text",)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
output=[
|
||||||
|
OutputBinding(
|
||||||
|
source=LocalPath(("echoed",)),
|
||||||
|
target=StatePath(("echoed",)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert service.draft_workspace_store is not None
|
||||||
|
workspace = service.draft_workspace_store.get_workspace("echo_draft")
|
||||||
|
|
||||||
|
assert result["workspace_id"] == "echo_draft"
|
||||||
|
assert workspace.draft["steps"]["call"]["in"] == {"input.text": "text"}
|
||||||
|
assert workspace.draft["steps"]["call"]["out"] == {"echoed": "state.echoed"}
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> None:
|
def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
local_temp_root() / "surface_workspace_from_capability"
|
local_temp_root() / "surface_workspace_from_capability"
|
||||||
|
|||||||
Reference in New Issue
Block a user