alright uhh

This commit is contained in:
lda
2026-05-18 01:40:50 +07:00 Verified
parent 9e7f91a223
commit d29688ec26
10 changed files with 241 additions and 5 deletions
+54
View File
@@ -409,6 +409,60 @@ The practical split:
- migrate: changed workflow behavior, incompatible schema, or different
provider semantics
## Artifact References
Reusable saved artifacts should prefer logical capability references, not
account-specific concrete references.
For example, if discovery shows a concrete node spec:
```text
demo.personal.echo_tool
```
and the author wants the artifact to depend on the logical source `demo`, the
saved plan should use:
```text
demo.echo_tool
```
and the artifact should declare a matching required capability:
```text
RequiredCapability(
logical_source="demo",
capability_name="echo_tool",
kind="node_spec"
)
```
The deployment then chooses the concrete account or connection profile:
```text
bindings:
demo: demo.personal
```
This keeps reusable artifacts portable across accounts while still letting
runtime validation prove that the concrete deployment can actually supply the
required capability.
Concrete references such as `demo.personal.echo_tool` remain useful for raw
local plans, tests, direct calls, and backward compatibility. Artifact creation
tools should avoid saving those concrete names by default when the user is
creating a reusable workflow or wrapper.
Planned authoring helper:
```text
concrete discovered ref + logical source alias -> artifact ref
demo.personal.echo_tool + demo -> demo.echo_tool
```
That helper should also populate `required_capabilities` so LLM clients do not
have to reverse-engineer dependency metadata from formatted names.
Bindings should live outside the immutable artifact, preferably on a deployment
or run configuration:
+12
View File
@@ -227,6 +227,18 @@ Saved wrapper artifacts can be called with a deployment id when they use logical
source names. The deployment supplies the concrete source bindings for that
test call, matching the way `run_deployment` resolves a full saved workflow.
Reusable wrapper and workflow artifacts should be authored against logical
source names by default. If an author discovers a concrete capability such as
`everything.default.echo`, the saved artifact should normally depend on a
logical reference such as `everything.echo` plus a required capability entry.
The deployment is responsible for binding `everything` to `everything.default`,
`everything.work`, or any other compatible concrete source.
This is especially important for LLM-authored workflows. The LLM should not
need to infer dependency metadata by parsing formatted names, and a saved
artifact should not accidentally become tied to the first account used during
exploration.
That direct-call surface is different from:
- calling the raw upstream MCP tool
+3
View File
@@ -17,6 +17,7 @@ from .models import (
)
from .store import FileWorkflowArtifactStore, WorkflowArtifactStore
from .validation import validate_deployment_dependencies
from .references import logical_ref_for_concrete_ref, normalize_plan_node_refs
__all__ = [
"AvailableCapability",
@@ -34,5 +35,7 @@ __all__ = [
"artifact_catalog_entry",
"artifact_node_name",
"create_workflow_artifact_from_plan",
"logical_ref_for_concrete_ref",
"normalize_plan_node_refs",
"validate_deployment_dependencies",
]
+12 -5
View File
@@ -5,6 +5,7 @@ from collections.abc import Mapping
from wf_core import ReducerRef, Workflow
from .models import ArtifactKind, JsonObject, RequiredCapability, WorkflowArtifact
from .references import normalize_plan_node_refs
def create_workflow_artifact_from_plan(
@@ -17,22 +18,28 @@ def create_workflow_artifact_from_plan(
kind: ArtifactKind = "workflow",
description: str | None = None,
required_capabilities: Mapping[str, RequiredCapability] | None = None,
source_bindings: Mapping[str, str] | None = None,
created_from_catalog_version: str | None = None,
) -> WorkflowArtifact:
"""Create an immutable artifact from a declarative workflow plan."""
_validate_workflow_plan(plan)
normalized_plan, node_requirements = normalize_plan_node_refs(
plan,
source_bindings or {},
)
_validate_workflow_plan(normalized_plan)
return WorkflowArtifact(
id=artifact_id,
version=version,
title=title,
kind=kind,
description=description,
input_schema=_required_object_field(plan, "input_schema"),
output_schema=_required_object_field(plan, "output_schema"),
input_schema=_required_object_field(normalized_plan, "input_schema"),
output_schema=_required_object_field(normalized_plan, "output_schema"),
outcomes=outcomes,
plan=plan,
plan=normalized_plan,
required_capabilities={
**_required_reducers_from_plan(plan),
**_required_reducers_from_plan(normalized_plan),
**node_requirements,
**dict(required_capabilities or {}),
},
created_from_catalog_version=created_from_catalog_version,
+73
View File
@@ -0,0 +1,73 @@
from __future__ import annotations
from collections.abc import Mapping
from copy import deepcopy
from .models import JsonObject, RequiredCapability
def normalize_plan_node_refs(
plan: JsonObject,
source_bindings: Mapping[str, str],
) -> tuple[JsonObject, dict[str, RequiredCapability]]:
"""Rewrite concrete node refs in a plan to deployment-bound logical refs.
`source_bindings` uses the same direction as `WorkflowDeployment.bindings`:
logical source -> concrete source. This lets artifact creation accept the
source mapping authors already need at deployment time while saving portable
plan refs such as `demo.echo_tool` instead of `demo.personal.echo_tool`.
"""
if not source_bindings:
return deepcopy(plan), {}
normalized = deepcopy(plan)
requirements: dict[str, RequiredCapability] = {}
nodes = normalized.get("nodes")
if not isinstance(nodes, list):
return normalized, requirements
for node in nodes:
if not isinstance(node, dict):
continue
raw_node_ref = node.get("node")
if not isinstance(raw_node_ref, str):
continue
replacement = logical_ref_for_concrete_ref(raw_node_ref, source_bindings)
if replacement is None:
continue
logical_ref, logical_source, capability_name, concrete_source = replacement
node["node"] = logical_ref
requirements[logical_ref] = RequiredCapability(
logical_source=logical_source,
capability_name=capability_name,
kind="node_spec",
observed_concrete_source=concrete_source,
)
return normalized, requirements
def logical_ref_for_concrete_ref(
concrete_ref: str,
source_bindings: Mapping[str, str],
) -> tuple[str, str, str, str] | None:
"""Return a logical ref for one concrete capability ref when bound.
The longest concrete source match wins so `demo.personal.pro.echo` can be
handled safely when both `demo.personal` and `demo.personal.pro` exist.
"""
matches = sorted(
source_bindings.items(),
key=lambda item: len(item[1]),
reverse=True,
)
for logical_source, concrete_source in matches:
prefix = f"{concrete_source}."
if not concrete_ref.startswith(prefix):
continue
capability_name = concrete_ref[len(prefix) :]
if not capability_name:
continue
logical_ref = f"{logical_source}.{capability_name}"
return logical_ref, logical_source, capability_name, concrete_source
return None
+2
View File
@@ -30,6 +30,7 @@ def register_artifact_tools(server: FastMCP, service: WfMcpService) -> None:
outcomes: list[str],
description: str | None = None,
required_capabilities: dict[str, dict[str, Any]] | None = None,
source_bindings: dict[str, str] | None = None,
created_from_catalog_version: str | None = None,
) -> dict[str, Any]:
return await handlers.create_artifact_from_plan(
@@ -43,6 +44,7 @@ def register_artifact_tools(server: FastMCP, service: WfMcpService) -> None:
name: capability
for name, capability in (required_capabilities or {}).items()
},
source_bindings=source_bindings,
created_from_catalog_version=created_from_catalog_version,
)
+2
View File
@@ -157,6 +157,7 @@ class WorkflowSurfaceHandlers:
kind: ArtifactKind = "workflow",
description: str | None = None,
required_capabilities: dict[str, dict[str, Any]] | None = None,
source_bindings: dict[str, str] | None = None,
created_from_catalog_version: str | None = None,
) -> dict[str, Any]:
if self.service.artifact_store is None:
@@ -178,6 +179,7 @@ class WorkflowSurfaceHandlers:
name: RequiredCapability.model_validate(capability)
for name, capability in (required_capabilities or {}).items()
},
source_bindings=source_bindings,
created_from_catalog_version=created_from_catalog_version,
)
self.service.artifact_store.save_artifact(workflow_artifact)
+2
View File
@@ -68,6 +68,7 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
required_capabilities: (
Mapping[str, RequiredCapability | dict[str, Any]] | None
) = None,
source_bindings: Mapping[str, str] | None = None,
created_from_catalog_version: str | None = None,
) -> dict[str, Any]:
return await handlers.create_artifact_from_plan(
@@ -87,6 +88,7 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
for name, capability in (required_capabilities or {}).items()
}
or None,
source_bindings=dict(source_bindings or {}),
created_from_catalog_version=created_from_catalog_version,
)
+57
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
from typing import Any, cast
from wf_artifacts import RequiredCapability, create_workflow_artifact_from_plan
@@ -53,6 +55,55 @@ def test_create_workflow_artifact_from_plan_adds_reducer_dependencies() -> None:
assert reducer.kind == "reducer"
def test_create_workflow_artifact_from_plan_rewrites_bound_node_specs() -> None:
plan = _plan()
_set_first_node_ref(plan, "demo.personal.echo_tool")
artifact = create_workflow_artifact_from_plan(
artifact_id="echo",
version=1,
title="Echo",
plan=plan,
outcomes=("done",),
source_bindings={"demo": "demo.personal"},
)
node = artifact.plan["nodes"][0]
required = artifact.required_capabilities["demo.echo_tool"]
assert node["node"] == "demo.echo_tool"
assert required.logical_source == "demo"
assert required.capability_name == "echo_tool"
assert required.kind == "node_spec"
assert required.observed_concrete_source == "demo.personal"
def test_create_workflow_artifact_from_plan_keeps_explicit_capability_metadata() -> (
None
):
plan = _plan()
_set_first_node_ref(plan, "demo.personal.echo_tool")
artifact = create_workflow_artifact_from_plan(
artifact_id="echo",
version=1,
title="Echo",
plan=plan,
outcomes=("done",),
source_bindings={"demo": "demo.personal"},
required_capabilities={
"demo.echo_tool": RequiredCapability(
logical_source="demo",
capability_name="echo_tool",
kind="node_spec",
input_schema_hash="sha256:explicit",
)
},
)
required = artifact.required_capabilities["demo.echo_tool"]
assert required.input_schema_hash == "sha256:explicit"
def test_create_workflow_artifact_from_plan_accepts_wrapper_kind() -> None:
artifact = create_workflow_artifact_from_plan(
artifact_id="normalize_status",
@@ -147,3 +198,9 @@ def _plan() -> dict[str, object]:
],
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
}
def _set_first_node_ref(plan: dict[str, object], node_ref: str) -> None:
"""Set the first node ref in a loosely typed raw plan test fixture."""
nodes = cast("list[dict[str, Any]]", plan["nodes"])
nodes[0]["node"] = node_ref
+24
View File
@@ -134,6 +134,30 @@ def test_workflow_surface_creates_wrapper_artifact_from_plan() -> None:
assert artifact.kind == "wrapper"
def test_workflow_surface_creates_artifact_with_logical_node_refs() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_logical_refs"
)
handlers = _handlers(artifact_store)
plan = _echo_artifact().plan
plan["nodes"][0]["node"] = "demo.personal.echo_tool"
asyncio.run(
handlers.create_artifact_from_plan(
artifact_id="echo_logical",
version=1,
title="Echo Logical",
plan=plan,
outcomes=("completed",),
source_bindings={"demo": "demo.personal"},
)
)
artifact = artifact_store.get_artifact("echo_logical", 1)
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
assert artifact.required_capabilities["demo.echo_tool"].logical_source == "demo"
def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
plan = RawWorkflowPlan.model_validate(_echo_artifact().plan)