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 - migrate: changed workflow behavior, incompatible schema, or different
provider semantics 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 Bindings should live outside the immutable artifact, preferably on a deployment
or run configuration: 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 source names. The deployment supplies the concrete source bindings for that
test call, matching the way `run_deployment` resolves a full saved workflow. 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: That direct-call surface is different from:
- calling the raw upstream MCP tool - calling the raw upstream MCP tool
+3
View File
@@ -17,6 +17,7 @@ from .models import (
) )
from .store import FileWorkflowArtifactStore, WorkflowArtifactStore from .store import FileWorkflowArtifactStore, WorkflowArtifactStore
from .validation import validate_deployment_dependencies from .validation import validate_deployment_dependencies
from .references import logical_ref_for_concrete_ref, normalize_plan_node_refs
__all__ = [ __all__ = [
"AvailableCapability", "AvailableCapability",
@@ -34,5 +35,7 @@ __all__ = [
"artifact_catalog_entry", "artifact_catalog_entry",
"artifact_node_name", "artifact_node_name",
"create_workflow_artifact_from_plan", "create_workflow_artifact_from_plan",
"logical_ref_for_concrete_ref",
"normalize_plan_node_refs",
"validate_deployment_dependencies", "validate_deployment_dependencies",
] ]
+12 -5
View File
@@ -5,6 +5,7 @@ from collections.abc import Mapping
from wf_core import ReducerRef, Workflow from wf_core import ReducerRef, Workflow
from .models import ArtifactKind, JsonObject, RequiredCapability, WorkflowArtifact from .models import ArtifactKind, JsonObject, RequiredCapability, WorkflowArtifact
from .references import normalize_plan_node_refs
def create_workflow_artifact_from_plan( def create_workflow_artifact_from_plan(
@@ -17,22 +18,28 @@ def create_workflow_artifact_from_plan(
kind: ArtifactKind = "workflow", kind: ArtifactKind = "workflow",
description: str | None = None, description: str | None = None,
required_capabilities: Mapping[str, RequiredCapability] | None = None, required_capabilities: Mapping[str, RequiredCapability] | None = None,
source_bindings: Mapping[str, str] | None = None,
created_from_catalog_version: str | None = None, created_from_catalog_version: str | None = None,
) -> WorkflowArtifact: ) -> WorkflowArtifact:
"""Create an immutable artifact from a declarative workflow plan.""" """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( return WorkflowArtifact(
id=artifact_id, id=artifact_id,
version=version, version=version,
title=title, title=title,
kind=kind, kind=kind,
description=description, description=description,
input_schema=_required_object_field(plan, "input_schema"), input_schema=_required_object_field(normalized_plan, "input_schema"),
output_schema=_required_object_field(plan, "output_schema"), output_schema=_required_object_field(normalized_plan, "output_schema"),
outcomes=outcomes, outcomes=outcomes,
plan=plan, plan=normalized_plan,
required_capabilities={ required_capabilities={
**_required_reducers_from_plan(plan), **_required_reducers_from_plan(normalized_plan),
**node_requirements,
**dict(required_capabilities or {}), **dict(required_capabilities or {}),
}, },
created_from_catalog_version=created_from_catalog_version, 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], outcomes: list[str],
description: str | None = None, description: str | None = None,
required_capabilities: dict[str, dict[str, Any]] | 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, created_from_catalog_version: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
return await handlers.create_artifact_from_plan( return await handlers.create_artifact_from_plan(
@@ -43,6 +44,7 @@ def register_artifact_tools(server: FastMCP, service: WfMcpService) -> None:
name: capability name: capability
for name, capability in (required_capabilities or {}).items() for name, capability in (required_capabilities or {}).items()
}, },
source_bindings=source_bindings,
created_from_catalog_version=created_from_catalog_version, created_from_catalog_version=created_from_catalog_version,
) )
+2
View File
@@ -157,6 +157,7 @@ class WorkflowSurfaceHandlers:
kind: ArtifactKind = "workflow", kind: ArtifactKind = "workflow",
description: str | None = None, description: str | None = None,
required_capabilities: dict[str, dict[str, Any]] | 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, created_from_catalog_version: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
if self.service.artifact_store is None: if self.service.artifact_store is None:
@@ -178,6 +179,7 @@ class WorkflowSurfaceHandlers:
name: RequiredCapability.model_validate(capability) name: RequiredCapability.model_validate(capability)
for name, capability in (required_capabilities or {}).items() for name, capability in (required_capabilities or {}).items()
}, },
source_bindings=source_bindings,
created_from_catalog_version=created_from_catalog_version, created_from_catalog_version=created_from_catalog_version,
) )
self.service.artifact_store.save_artifact(workflow_artifact) 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: ( required_capabilities: (
Mapping[str, RequiredCapability | dict[str, Any]] | None Mapping[str, RequiredCapability | dict[str, Any]] | None
) = None, ) = None,
source_bindings: Mapping[str, str] | None = None,
created_from_catalog_version: str | None = None, created_from_catalog_version: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
return await handlers.create_artifact_from_plan( 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() for name, capability in (required_capabilities or {}).items()
} }
or None, or None,
source_bindings=dict(source_bindings or {}),
created_from_catalog_version=created_from_catalog_version, created_from_catalog_version=created_from_catalog_version,
) )
+57
View File
@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, cast
from wf_artifacts import RequiredCapability, create_workflow_artifact_from_plan 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" 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: def test_create_workflow_artifact_from_plan_accepts_wrapper_kind() -> None:
artifact = create_workflow_artifact_from_plan( artifact = create_workflow_artifact_from_plan(
artifact_id="normalize_status", artifact_id="normalize_status",
@@ -147,3 +198,9 @@ def _plan() -> dict[str, object]:
], ],
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}], "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" 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: def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
plan = RawWorkflowPlan.model_validate(_echo_artifact().plan) plan = RawWorkflowPlan.model_validate(_echo_artifact().plan)