more model; now for serializing

This commit is contained in:
lda
2026-05-18 03:13:43 +07:00 Verified
parent b288671aee
commit 7534bf9de0
10 changed files with 109 additions and 4 deletions
+4
View File
@@ -194,6 +194,10 @@ The code now has the first capability-source layer in place.
- `CapabilitySource` is the mutable runtime registry object; typed
`SourceStatus` and `SourceInventory` snapshots are the serializable domain
projections used at boundaries such as `list_sources()`.
- Executable `NodeSpec` objects stay inside runtime buckets because they hold
Python callables. Inventory exposes `NodeSpecInventory` contracts instead:
names, descriptions, outcomes, schemas, and execution flags, but never the
wrapped function object itself.
- `WfMcpService.capability_sources` is the canonical in-memory registry.
- Planner node lookup reads `CapabilitySource.capabilities.node_specs`
directly; the old `SpecSource` compatibility layer has been removed.
+6 -2
View File
@@ -84,10 +84,14 @@ class PathExpr:
def le(self, other: object) -> Expr:
return self._binary("le", other)
def __eq__(self, other: object) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[override] # ty: ignore[invalid-method-override]
def __eq__( # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[override] # ty: ignore[invalid-method-override]
self, other: object
) -> Expr:
return self._binary("eq", other)
def __ne__(self, other: object) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[override] # ty: ignore[invalid-method-override]
def __ne__( # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[override] # ty: ignore[invalid-method-override]
self, other: object
) -> Expr:
return self._binary("ne", other)
def __gt__(self, other: object) -> Expr:
+1 -1
View File
@@ -34,5 +34,5 @@ class Workflow(BaseModel):
def validate_structure(self) -> "ValidationReport":
"""Return all structural validation issues for this workflow."""
validation = import_module("wf_core.validation.core")
import wf_core.validation.core as validation
return cast("ValidationReport", validation.validate_workflow(self))
+2
View File
@@ -2,6 +2,7 @@ from .refs import CapabilityRef, SourceRef
from .sources import (
CapabilityBuckets,
CapabilitySource,
NodeSpecInventory,
SourceCapabilityInventory,
SourceInventory,
SourceKind,
@@ -16,6 +17,7 @@ __all__ = [
"CapabilityBuckets",
"CapabilitySource",
"CapabilityRef",
"NodeSpecInventory",
"SourceCapabilityInventory",
"SourceInventory",
"SourceKind",
+35
View File
@@ -8,6 +8,7 @@ from wf_core import ReducerSpec
from wf_core.runtime.ops.merges import ReducerDefinition
SourceKind = Literal["system", "connection"]
JsonObject = dict[str, Any]
@dataclass(frozen=True, slots=True)
@@ -42,11 +43,24 @@ class SourcePermissionsSnapshot(BaseModel):
mutates_auth: bool = False
class NodeSpecInventory(BaseModel):
"""Serializable public contract for one executable node spec."""
name: str
description: str | None = None
outcomes: tuple[str, ...]
input_schema: JsonObject
output_schema: JsonObject
is_async: bool
accepts_context: bool
class SourceCapabilityInventory(BaseModel):
"""Serializable names owned by one source, grouped by capability kind."""
tools: tuple[str, ...] = ()
node_specs: tuple[str, ...] = ()
node_spec_details: tuple[NodeSpecInventory, ...] = ()
reducers: tuple[str, ...] = ()
prompts: tuple[str, ...] = ()
resources: tuple[str, ...] = ()
@@ -126,8 +140,29 @@ class CapabilitySource:
capabilities=SourceCapabilityInventory(
tools=tuple(sorted(self.capabilities.tools)),
node_specs=tuple(sorted(self.capabilities.node_specs)),
node_spec_details=tuple(
_node_spec_inventory(spec)
for spec in sorted(
self.capabilities.node_specs.values(),
key=lambda spec: spec.name,
)
),
reducers=tuple(sorted(self.capabilities.reducers)),
prompts=tuple(sorted(self.capabilities.prompts)),
resources=tuple(sorted(self.capabilities.resources)),
),
)
def _node_spec_inventory(spec: NodeSpec[Any, Any]) -> NodeSpecInventory:
"""Project one executable node spec into a serializable public contract."""
node_def = spec.to_node_def()
return NodeSpecInventory(
name=spec.name,
description=spec.description,
outcomes=tuple(node_def.outcomes),
input_schema=node_def.input_schema.model_dump(mode="json"),
output_schema=node_def.output_schema.model_dump(mode="json"),
is_async=spec.is_async,
accepts_context=spec.accepts_context,
)
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
from pydantic import BaseModel, Field
from wf_authoring import node
from wf_platform import CapabilityBuckets, CapabilitySource, NodeSpecInventory
class EchoInput(BaseModel):
text: str = Field(description="Text to echo.")
class EchoOutput(BaseModel):
echoed: str
@node
def echo(payload: EchoInput) -> EchoOutput:
"""Echo one text field."""
return EchoOutput(echoed=payload.text)
def test_source_inventory_exposes_serializable_node_spec_details() -> None:
source = CapabilitySource(
id="demo.personal",
kind="connection",
capabilities=CapabilityBuckets(node_specs={echo.name: echo}),
)
inventory = source.as_inventory()
detail = inventory.capabilities.node_spec_details[0]
dumped = inventory.model_dump(mode="json")
assert isinstance(detail, NodeSpecInventory)
assert detail.name == echo.name
assert detail.description == "Echo one text field."
assert detail.outcomes == ("ok",)
assert detail.input_schema["properties"]["text"]["description"] == "Text to echo."
assert "fn" not in dumped["capabilities"]["node_spec_details"][0]
+4
View File
@@ -3,3 +3,7 @@
so you know im using ts and influencing its design. im not just an user (lowk i am)
[llgd]: <https://git.ldlda.com/lda/langgraph-demo>
## to agents
these are not to be massively shuffled. you can help me; inform your edits.
+18 -1
View File
@@ -1,4 +1,5 @@
from tests.authoring.test_reducers import modulo_add
from pydantic import BaseModel, Field
from tests.rewrite.actions import (
CounterUp,
RateChange,
@@ -14,8 +15,24 @@ from tests.rewrite.models import Input, State, Storage
from wf_authoring.builder import WorkflowBuilder
from wf_authoring.dsl.conditions import expr, state
from wf_authoring.reducers.catalog import ReducerCatalog
from wf_authoring.reducers.decorator import reducer
from wf_core.tokens import END
class ModuloConfig(BaseModel):
modulus: int = Field(gt=0)
@reducer(name="wf.std.modulo_add", config_model=ModuloConfig)
def modulo_add(
current: int | None,
incoming: int,
config: ModuloConfig,
) -> int:
"""Add incoming values modulo a configured positive integer."""
return ((current or 0) + incoming) % config.modulus
# alr so we start with workflowbuilder.
gacha = WorkflowBuilder(