capabilityref: less string matching more model

This commit is contained in:
lda
2026-05-18 02:32:02 +07:00 Verified
parent cb8d43a158
commit 90e2141be3
8 changed files with 137 additions and 27 deletions
+5 -6
View File
@@ -571,12 +571,11 @@ concrete source but leaves the saved artifact immutable. Concrete node names
such as `demo.personal.echo_tool` remain supported for raw local plans and older
artifacts.
Implementation note: these references are currently parsed from strings with
dot-separated source and capability names. That keeps the wire format simple but
leaks path logic into runtime code. A future cleanup should introduce typed
reference objects, such as `CapabilityRef(logical_source, capability_name)` and
`BoundCapabilityRef(concrete_source, capability_name)`, and leave dot-joined
strings as presentation and serialization only.
Implementation note: source and capability names now have segment-backed
platform refs: `SourceRef(parts=...)` and `CapabilityRef(source=..., name=...)`.
Dot-joined names remain the wire/presentation format, but new runtime code
should parse or format through those refs instead of rediscovering source/name
boundaries with ad hoc string splits.
The first implementation should prefer artifact validation and dependency
diagnostics before attempting persistent nested resume.
+6 -6
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Mapping
from wf_core import ReducerRef, Workflow
from wf_platform import CapabilityRef
from .models import ArtifactKind, JsonObject, RequiredCapability, WorkflowArtifact
from .references import normalize_plan_node_refs
@@ -101,14 +102,13 @@ def _required_reducers_from_plan(plan: JsonObject) -> dict[str, RequiredCapabili
reducer = ReducerRef.model_validate(reducer_payload)
except ValueError:
continue
if "." not in reducer.name:
try:
reducer_ref = CapabilityRef.parse(reducer.name)
except ValueError:
continue
if "." not in reducer.name:
continue
logical_source, _, capability_name = reducer.name.rpartition(".")
requirements[reducer.name] = RequiredCapability(
logical_source=logical_source,
capability_name=capability_name,
logical_source=str(reducer_ref.source),
capability_name=reducer_ref.name,
kind="reducer",
)
return requirements
+5 -1
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from collections.abc import Mapping
from copy import deepcopy
from wf_platform import CapabilityRef, SourceRef
from .models import JsonObject, RequiredCapability
@@ -68,6 +70,8 @@ def logical_ref_for_concrete_ref(
capability_name = concrete_ref[len(prefix) :]
if not capability_name:
continue
logical_ref = f"{logical_source}.{capability_name}"
logical_ref = str(
CapabilityRef(source=SourceRef.parse(logical_source), name=capability_name)
)
return logical_ref, logical_source, capability_name, concrete_source
return None
+15 -4
View File
@@ -15,6 +15,7 @@ from wf_artifacts import (
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
validate_deployment_dependencies,
)
from wf_platform import CapabilityRef
from wf_authoring import build_async_registry
from wf_core import RuntimeContext
@@ -316,21 +317,23 @@ def _available_sources(service: WfMcpService) -> list[AvailableSource]:
sources: list[AvailableSource] = []
for source in service.capability_sources.values():
capabilities = {
spec.name.rsplit(".", maxsplit=1)[-1]: AvailableCapability(
name=spec.name.rsplit(".", maxsplit=1)[-1],
capability_name: AvailableCapability(
name=capability_name,
kind="node_spec",
input_schema_hash=None,
output_schema_hash=None,
)
for spec in source.capabilities.node_specs.values()
if (capability_name := _capability_name(spec.name)) is not None
}
capabilities.update(
{
reducer.name.rsplit(".", maxsplit=1)[-1]: AvailableCapability(
name=reducer.name.rsplit(".", maxsplit=1)[-1],
capability_name: AvailableCapability(
name=capability_name,
kind="reducer",
)
for reducer in source.capabilities.reducers.values()
if (capability_name := _capability_name(reducer.name)) is not None
}
)
sources.append(
@@ -343,6 +346,14 @@ def _available_sources(service: WfMcpService) -> list[AvailableSource]:
return sources
def _capability_name(qualified_name: str) -> str | None:
"""Return the local name of one qualified capability ref if it is valid."""
try:
return CapabilityRef.parse(qualified_name).name
except ValueError:
return None
def _artifact_capability_id(artifact: WorkflowArtifact) -> str:
"""Use the same stable name shape as workflow artifact catalog entries."""
return f"workflow.{artifact.id}.v{artifact.version}"
@@ -6,7 +6,7 @@ from typing import Any
from wf_artifacts import RequiredCapability, WorkflowArtifact, WorkflowDeployment
from wf_authoring import AsyncRegistryHandler, NodeSpec, build_async_registry
from wf_core.runtime.ops.merges import ReducerDefinition
from wf_platform import CapabilitySource
from wf_platform import CapabilityRef, CapabilitySource
@dataclass(frozen=True, slots=True)
@@ -67,11 +67,12 @@ def _resolve_node_spec(
return node_name, concrete
if deployment is not None:
logical_source, separator, capability_name = node_name.rpartition(".")
if separator:
bound_source_id = deployment.bindings.get(logical_source)
if bound_source_id is not None:
bound_name = f"{bound_source_id}.{capability_name}"
try:
bound_ref = CapabilityRef.parse(node_name).bind(deployment.bindings)
except ValueError:
bound_ref = None
if bound_ref is not None:
bound_name = str(bound_ref)
concrete = _find_node_spec(bound_name, sources)
if concrete is not None:
return bound_name, concrete
@@ -124,6 +125,10 @@ def _find_reducer_definition(
capability_name: str,
) -> ReducerDefinition | None:
for reducer_name, definition in source.capabilities.reducer_definitions.items():
if reducer_name.rsplit(".", maxsplit=1)[-1] == capability_name:
try:
reducer_ref = CapabilityRef.parse(reducer_name)
except ValueError:
continue
if reducer_ref.name == capability_name:
return definition
return None
+3
View File
@@ -1,3 +1,4 @@
from .refs import CapabilityRef, SourceRef
from .sources import (
CapabilityBuckets,
CapabilitySource,
@@ -14,6 +15,7 @@ from .sources import (
__all__ = [
"CapabilityBuckets",
"CapabilitySource",
"CapabilityRef",
"SourceCapabilityInventory",
"SourceInventory",
"SourceKind",
@@ -22,4 +24,5 @@ __all__ = [
"SourceStatus",
"SourceVisibility",
"SourceVisibilitySnapshot",
"SourceRef",
]
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class SourceRef:
"""Segment-backed source identifier with dotted-string wire formatting."""
parts: tuple[str, ...]
def __post_init__(self) -> None:
if not self.parts or any(not part for part in self.parts):
raise ValueError("source ref requires non-empty path segments")
@classmethod
def parse(cls, value: str) -> SourceRef:
"""Parse one dotted source id into first-class path segments."""
return cls(tuple(value.split(".")))
def __str__(self) -> str:
return ".".join(self.parts)
@dataclass(frozen=True, slots=True)
class CapabilityRef:
"""Segment-backed capability reference: one source plus one local name."""
source: SourceRef
name: str
def __post_init__(self) -> None:
if not self.name:
raise ValueError("capability ref requires a non-empty name")
@classmethod
def parse(cls, value: str) -> CapabilityRef:
"""Parse `<source>.<capability>` while preserving source path segments."""
source_text, separator, name = value.rpartition(".")
if not separator or not source_text or not name:
raise ValueError("capability ref requires source and capability segments")
return cls(source=SourceRef.parse(source_text), name=name)
def bind(self, bindings: Mapping[str, str]) -> CapabilityRef:
"""Replace a logical source with its concrete bound source when present."""
bound_source = bindings.get(str(self.source))
if bound_source is None:
return self
return CapabilityRef(source=SourceRef.parse(bound_source), name=self.name)
def __str__(self) -> str:
return f"{self.source}.{self.name}"
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
from wf_platform import CapabilityRef, SourceRef
def test_source_ref_round_trips_segmented_names() -> None:
source = SourceRef.parse("demo.personal")
assert source.parts == ("demo", "personal")
assert str(source) == "demo.personal"
def test_capability_ref_round_trips_segmented_names() -> None:
ref = CapabilityRef.parse("demo.personal.echo_tool")
assert ref.source.parts == ("demo", "personal")
assert ref.name == "echo_tool"
assert str(ref) == "demo.personal.echo_tool"
def test_capability_ref_binds_logical_source_to_concrete_source() -> None:
ref = CapabilityRef.parse("demo.echo_tool")
bound = ref.bind({"demo": "demo.personal"})
assert str(bound) == "demo.personal.echo_tool"
def test_capability_ref_rejects_missing_capability_name() -> None:
try:
CapabilityRef.parse("demo")
except ValueError as exc:
assert "source and capability" in str(exc)
else:
raise AssertionError("expected invalid capability ref to be rejected")