third slice: artifacts and deployments...
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .artifacts import WorkflowArtifactApi
|
||||
from .backend import TraceRange, WorkflowApiBackend
|
||||
from .constants import (
|
||||
DEFAULT_CALL_STEP_ID,
|
||||
@@ -8,6 +9,7 @@ from .constants import (
|
||||
DEFAULT_OK_OUTCOME,
|
||||
RUNTIME_ERROR_CAPABILITY,
|
||||
)
|
||||
from .deployments import WorkflowDeploymentApi
|
||||
from .drafts import WorkflowDraftApi
|
||||
from .next_actions import NextActionPatchExample, NextActionTool, NextActions
|
||||
from .refs import WorkflowSurfaceCapabilityId, parse_workflow_surface_capability_id
|
||||
@@ -52,7 +54,9 @@ __all__ = [
|
||||
"TraceRange",
|
||||
"WorkflowApi",
|
||||
"WorkflowApiBackend",
|
||||
"WorkflowArtifactApi",
|
||||
"WorkflowArtifactCataloger",
|
||||
"WorkflowDeploymentApi",
|
||||
"WorkflowDraftApi",
|
||||
"WorkflowEventRecorder",
|
||||
"WorkflowLiveSourceChecker",
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
"""Saved workflow artifact operations.
|
||||
|
||||
Event construction is intentionally delegated through
|
||||
WorkflowOperationContext so this module stays protocol-neutral.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from wf_artifacts import (
|
||||
ArtifactKind,
|
||||
RequiredCapability,
|
||||
WorkflowArtifact,
|
||||
WorkflowCapabilityRef,
|
||||
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
|
||||
)
|
||||
from wf_platform import NodeSpecInventory, page_items
|
||||
|
||||
from .drafts import WorkflowDraftApi
|
||||
from .models import RawWorkflowPlan
|
||||
from .operation_context import WorkflowOperationContext
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def _matches_query(*values: object, query: str | None) -> bool:
|
||||
"""Return whether a compact discovery row matches a human search query."""
|
||||
if query is None:
|
||||
return True
|
||||
needle = query.strip().casefold()
|
||||
if not needle:
|
||||
return True
|
||||
return any(needle in str(value).casefold() for value in values if value is not None)
|
||||
|
||||
|
||||
def _paged_list_payload(
|
||||
key: str,
|
||||
items: Sequence[T],
|
||||
*,
|
||||
cursor: str | None,
|
||||
limit: int,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the common workflow-surface list response shape."""
|
||||
page = page_items(items, cursor=cursor, limit=limit)
|
||||
return {
|
||||
key: list(page.items),
|
||||
"next_cursor": page.next_cursor,
|
||||
"total": page.total,
|
||||
}
|
||||
|
||||
|
||||
class WorkflowArtifactApi:
|
||||
"""Saved workflow artifact operations.
|
||||
|
||||
Event construction is intentionally delegated through
|
||||
WorkflowOperationContext so this module stays protocol-neutral.
|
||||
"""
|
||||
|
||||
def __init__(self, context: WorkflowOperationContext) -> None:
|
||||
self.context = context
|
||||
self.drafts = WorkflowDraftApi(context)
|
||||
|
||||
def _artifact_store(self):
|
||||
if self.context.artifact_store is None:
|
||||
raise KeyError("workflow artifact store is not configured")
|
||||
return self.context.artifact_store
|
||||
|
||||
async def list_artifacts(
|
||||
self,
|
||||
*,
|
||||
query: str | None = None,
|
||||
kind: ArtifactKind | None = None,
|
||||
cursor: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> dict[str, Any]:
|
||||
"""Return compact paged saved artifact summaries.
|
||||
|
||||
Saved artifacts can contain full raw workflow plans, so list results
|
||||
deliberately stay summary-only. Use inspect/run tools for detail.
|
||||
"""
|
||||
if self.context.artifact_store is None:
|
||||
return _paged_list_payload("nodes", [], cursor=cursor, limit=limit)
|
||||
entries = [
|
||||
self.context.artifacts.workflow_artifact_catalog_entry(artifact).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
for artifact in self.context.artifact_store.list_artifacts()
|
||||
if kind is None or artifact.kind == kind
|
||||
]
|
||||
entries = [
|
||||
entry
|
||||
for entry in entries
|
||||
if _matches_query(
|
||||
entry.get("name"),
|
||||
entry.get("artifact_id"),
|
||||
entry.get("display_name"),
|
||||
entry.get("description"),
|
||||
entry.get("kind"),
|
||||
query=query,
|
||||
)
|
||||
]
|
||||
entries.sort(key=lambda entry: str(entry["name"]))
|
||||
return _paged_list_payload("nodes", entries, cursor=cursor, limit=limit)
|
||||
|
||||
async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]:
|
||||
workflow_artifact = WorkflowArtifact.model_validate(artifact)
|
||||
self._artifact_store().save_artifact(workflow_artifact)
|
||||
self.context.events.record_workflow_event(
|
||||
"workflow_artifact_saved",
|
||||
capability_id=_artifact_capability_id(workflow_artifact),
|
||||
payload={
|
||||
"artifact_id": workflow_artifact.id,
|
||||
"version": workflow_artifact.version,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"artifact_id": workflow_artifact.id,
|
||||
"version": workflow_artifact.version,
|
||||
"saved": True,
|
||||
}
|
||||
|
||||
async def create_artifact_from_plan(
|
||||
self,
|
||||
*,
|
||||
artifact_id: str,
|
||||
version: int,
|
||||
title: str,
|
||||
plan: RawWorkflowPlan | dict[str, Any],
|
||||
outcomes: Sequence[str],
|
||||
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]:
|
||||
typed_plan = (
|
||||
plan
|
||||
if isinstance(plan, RawWorkflowPlan)
|
||||
else RawWorkflowPlan.model_validate(plan)
|
||||
)
|
||||
workflow_artifact = build_workflow_artifact_from_plan(
|
||||
artifact_id=artifact_id,
|
||||
version=version,
|
||||
title=title,
|
||||
kind=kind,
|
||||
description=description,
|
||||
plan=typed_plan.model_dump(mode="json", by_alias=True),
|
||||
outcomes=tuple(outcomes),
|
||||
required_capabilities={
|
||||
name: RequiredCapability.model_validate(capability)
|
||||
for name, capability in (required_capabilities or {}).items()
|
||||
},
|
||||
source_bindings=source_bindings,
|
||||
observed_node_specs=_observed_node_specs(self.context),
|
||||
created_from_catalog_version=created_from_catalog_version,
|
||||
)
|
||||
self._artifact_store().save_artifact(workflow_artifact)
|
||||
self.context.events.record_workflow_event(
|
||||
"workflow_artifact_saved",
|
||||
capability_id=_artifact_capability_id(workflow_artifact),
|
||||
payload={
|
||||
"artifact_id": workflow_artifact.id,
|
||||
"version": workflow_artifact.version,
|
||||
"created_from_plan": True,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"artifact_id": workflow_artifact.id,
|
||||
"version": workflow_artifact.version,
|
||||
"saved": True,
|
||||
}
|
||||
|
||||
async def create_artifact_from_draft(
|
||||
self,
|
||||
*,
|
||||
artifact_id: str,
|
||||
version: int,
|
||||
title: str,
|
||||
draft: dict[str, Any],
|
||||
outcomes: Sequence[str],
|
||||
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]:
|
||||
from wf_artifacts import compile_workflow_draft
|
||||
|
||||
plan = compile_workflow_draft(draft)
|
||||
workflow_artifact = build_workflow_artifact_from_plan(
|
||||
artifact_id=artifact_id,
|
||||
version=version,
|
||||
title=title,
|
||||
kind=kind,
|
||||
description=description,
|
||||
plan=plan,
|
||||
outcomes=tuple(outcomes),
|
||||
required_capabilities={
|
||||
name: RequiredCapability.model_validate(capability)
|
||||
for name, capability in (required_capabilities or {}).items()
|
||||
},
|
||||
source_bindings=source_bindings,
|
||||
observed_node_specs=_observed_node_specs(self.context),
|
||||
created_from_catalog_version=created_from_catalog_version,
|
||||
)
|
||||
self._artifact_store().save_artifact(workflow_artifact)
|
||||
self.context.events.record_workflow_event(
|
||||
"workflow_artifact_saved",
|
||||
capability_id=_artifact_capability_id(workflow_artifact),
|
||||
payload={
|
||||
"artifact_id": workflow_artifact.id,
|
||||
"version": workflow_artifact.version,
|
||||
"created_from_draft": True,
|
||||
},
|
||||
)
|
||||
required_sources = sorted(
|
||||
{
|
||||
capability.logical_source
|
||||
for capability in workflow_artifact.required_capability_map().values()
|
||||
}
|
||||
)
|
||||
return {
|
||||
"artifact_id": workflow_artifact.id,
|
||||
"version": workflow_artifact.version,
|
||||
"saved": True,
|
||||
"required_logical_sources": required_sources,
|
||||
"suggested_bindings": _suggested_self_bindings(required_sources),
|
||||
}
|
||||
|
||||
async def create_artifact_from_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
artifact_id: str,
|
||||
version: int,
|
||||
title: str,
|
||||
outcomes: Sequence[str],
|
||||
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]:
|
||||
store = self.context.draft_workspace_store
|
||||
if store is None:
|
||||
raise KeyError("draft workspace store is not configured")
|
||||
workspace = store.get_workspace(workspace_id)
|
||||
validation = await self.drafts.validate_draft(draft=workspace.draft)
|
||||
if validation["status"] != "valid":
|
||||
return {
|
||||
"saved": False,
|
||||
"workspace_id": workspace_id,
|
||||
"revision": workspace.revision,
|
||||
"status": validation["status"],
|
||||
"diagnostics": validation["diagnostics"],
|
||||
}
|
||||
return await self.create_artifact_from_draft(
|
||||
artifact_id=artifact_id,
|
||||
version=version,
|
||||
title=title,
|
||||
kind=kind,
|
||||
description=description,
|
||||
draft=workspace.draft,
|
||||
outcomes=outcomes,
|
||||
required_capabilities=required_capabilities,
|
||||
source_bindings=source_bindings,
|
||||
created_from_catalog_version=created_from_catalog_version,
|
||||
)
|
||||
|
||||
async def create_wrapper_from_workspace(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
artifact_id: str,
|
||||
version: int,
|
||||
title: str,
|
||||
outcomes: Sequence[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]:
|
||||
"""Save the current draft workspace as a callable wrapper artifact."""
|
||||
return await self.create_artifact_from_workspace(
|
||||
workspace_id=workspace_id,
|
||||
artifact_id=artifact_id,
|
||||
version=version,
|
||||
title=title,
|
||||
outcomes=outcomes,
|
||||
kind="wrapper",
|
||||
description=description,
|
||||
required_capabilities=required_capabilities,
|
||||
source_bindings=source_bindings,
|
||||
created_from_catalog_version=created_from_catalog_version,
|
||||
)
|
||||
|
||||
async def inspect_artifact(
|
||||
self, *, artifact_id: str, version: int
|
||||
) -> dict[str, Any]:
|
||||
artifact = self._artifact_store().get_artifact(artifact_id, version)
|
||||
return artifact.model_dump(mode="json")
|
||||
|
||||
|
||||
def _required_capability_payloads(
|
||||
requirements: dict[str, RequiredCapability],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
return {
|
||||
name: capability.model_dump(mode="json")
|
||||
for name, capability in sorted(requirements.items())
|
||||
}
|
||||
|
||||
|
||||
def _suggested_self_bindings(required_sources: Sequence[str]) -> dict[str, str]:
|
||||
"""Suggest local bindings for built-in sources that deploy to themselves."""
|
||||
return {
|
||||
source: source for source in required_sources if source in {"wf.std", "wf.mcp"}
|
||||
}
|
||||
|
||||
|
||||
def _observed_node_specs(
|
||||
context: WorkflowOperationContext,
|
||||
) -> dict[str, NodeSpecInventory]:
|
||||
"""Project current executable specs into serializable observed contracts."""
|
||||
observed: dict[str, NodeSpecInventory] = {}
|
||||
for source in context.capability_sources.values():
|
||||
inventory = source.as_inventory()
|
||||
observed.update(
|
||||
{detail.name: detail for detail in inventory.capabilities.node_spec_details}
|
||||
)
|
||||
return observed
|
||||
|
||||
|
||||
def _plan_nodes(artifact: WorkflowArtifact) -> list[dict[str, Any]]:
|
||||
nodes = artifact.plan.get("nodes", [])
|
||||
return [node for node in nodes if isinstance(node, dict)]
|
||||
|
||||
|
||||
def _artifact_capability_id(artifact: WorkflowArtifact) -> str:
|
||||
"""Use the same stable name shape as workflow artifact catalog entries."""
|
||||
return str(
|
||||
WorkflowCapabilityRef(
|
||||
artifact_id=artifact.id,
|
||||
version=artifact.version,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"WorkflowArtifactApi",
|
||||
]
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Saved deployment operations and dependency validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from wf_artifacts import (
|
||||
AvailableCapability,
|
||||
AvailableSource,
|
||||
DependencyDiagnostic,
|
||||
WorkflowArtifact,
|
||||
WorkflowDeployment,
|
||||
validate_deployment_dependencies,
|
||||
)
|
||||
from wf_platform import CapabilitySource, hash_json_schema
|
||||
|
||||
from .next_actions import NextActions
|
||||
from .operation_context import WorkflowOperationContext
|
||||
from .saved_subgraphs import resolve_saved_subgraph_tree, validate_saved_subgraph_tree
|
||||
|
||||
|
||||
class WorkflowDeploymentApi:
|
||||
"""Saved deployment operations and dependency validation."""
|
||||
|
||||
def __init__(self, context: WorkflowOperationContext) -> None:
|
||||
self.context = context
|
||||
|
||||
def _artifact_store(self):
|
||||
if self.context.artifact_store is None:
|
||||
raise KeyError("workflow artifact store is not configured")
|
||||
return self.context.artifact_store
|
||||
|
||||
async def list_deployments(self) -> dict[str, Any]:
|
||||
if self.context.artifact_store is None:
|
||||
return {"deployments": []}
|
||||
return {
|
||||
"deployments": [
|
||||
_deployment_summary(deployment)
|
||||
for deployment in self.context.artifact_store.list_deployments()
|
||||
]
|
||||
}
|
||||
|
||||
async def inspect_deployment(self, *, deployment_id: str) -> dict[str, Any]:
|
||||
return self._artifact_store().get_deployment(deployment_id).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
async def save_deployment(self, deployment: dict[str, Any]) -> dict[str, Any]:
|
||||
workflow_deployment = WorkflowDeployment.model_validate(deployment)
|
||||
self._artifact_store().save_deployment(workflow_deployment)
|
||||
self.context.events.record_workflow_event(
|
||||
"workflow_deployment_saved",
|
||||
capability_id=f"deployment.{workflow_deployment.id}",
|
||||
payload={
|
||||
"deployment_id": workflow_deployment.id,
|
||||
"artifact_id": workflow_deployment.artifact_id,
|
||||
"artifact_version": workflow_deployment.artifact_version,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"deployment_id": workflow_deployment.id,
|
||||
"artifact_id": workflow_deployment.artifact_id,
|
||||
"artifact_version": workflow_deployment.artifact_version,
|
||||
"saved": True,
|
||||
}
|
||||
|
||||
async def delete_deployment(self, *, deployment_id: str) -> dict[str, Any]:
|
||||
"""Delete one mutable deployment environment binding."""
|
||||
self._artifact_store().delete_deployment(deployment_id)
|
||||
self.context.events.record_workflow_event(
|
||||
"workflow_deployment_deleted",
|
||||
capability_id=f"deployment.{deployment_id}",
|
||||
payload={"deployment_id": deployment_id},
|
||||
)
|
||||
return {"deployment_id": deployment_id, "deleted": True}
|
||||
|
||||
async def validate_deployment(
|
||||
self,
|
||||
*,
|
||||
deployment_id: str,
|
||||
live_check: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
deployment, artifact, diagnostics, tree = self.deployment_validation(
|
||||
deployment_id
|
||||
)
|
||||
if live_check and self.context.live_sources is not None:
|
||||
diagnostics.extend(
|
||||
await self.context.live_sources.deployment_diagnostics(
|
||||
deployment=deployment,
|
||||
artifacts=[artifact, *tree.artifacts_by_ref.values()],
|
||||
)
|
||||
)
|
||||
return {
|
||||
"deployment_id": deployment.id,
|
||||
"artifact_id": artifact.id,
|
||||
"artifact_version": artifact.version,
|
||||
"status": "unrunnable" if diagnostics else "runnable",
|
||||
"diagnostics": [
|
||||
diagnostic.model_dump(mode="json") for diagnostic in diagnostics
|
||||
],
|
||||
"next_actions": NextActions.from_deployment_validation(
|
||||
deployment_id=deployment.id,
|
||||
diagnostics=diagnostics,
|
||||
).model_dump(mode="json"),
|
||||
}
|
||||
|
||||
def deployment_validation(
|
||||
self,
|
||||
deployment_id: str,
|
||||
) -> tuple[
|
||||
WorkflowDeployment,
|
||||
WorkflowArtifact,
|
||||
list[DependencyDiagnostic],
|
||||
Any, # SavedSubgraphTree
|
||||
]:
|
||||
store = self._artifact_store()
|
||||
deployment = store.get_deployment(deployment_id)
|
||||
artifact = store.get_artifact(
|
||||
deployment.artifact_id,
|
||||
deployment.artifact_version,
|
||||
)
|
||||
available_sources = _available_sources(self.context.capability_sources)
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact,
|
||||
deployment=deployment,
|
||||
sources=available_sources,
|
||||
)
|
||||
tree = resolve_saved_subgraph_tree(
|
||||
root_artifact=artifact,
|
||||
artifact_store=store,
|
||||
)
|
||||
diagnostics.extend(
|
||||
validate_saved_subgraph_tree(
|
||||
tree=tree,
|
||||
deployment=deployment,
|
||||
sources=available_sources,
|
||||
)
|
||||
)
|
||||
return deployment, artifact, diagnostics, tree
|
||||
|
||||
|
||||
def _available_sources(
|
||||
capability_sources: Mapping[str, CapabilitySource],
|
||||
) -> list[AvailableSource]:
|
||||
"""Convert broker capability sources into artifact validation snapshots."""
|
||||
sources: list[AvailableSource] = []
|
||||
for source in capability_sources.values():
|
||||
node_spec_details = {
|
||||
detail.name: detail
|
||||
for detail in source.as_inventory().capabilities.node_spec_details
|
||||
}
|
||||
capabilities = {
|
||||
capability_name: AvailableCapability(
|
||||
name=capability_name,
|
||||
kind="node_spec",
|
||||
input_schema_hash=hash_json_schema(detail.input_schema),
|
||||
output_schema_hash=hash_json_schema(detail.output_schema),
|
||||
)
|
||||
for spec in source.capabilities.node_specs.values()
|
||||
if (capability_name := _capability_name(spec.name)) is not None
|
||||
if (detail := node_spec_details.get(spec.name)) is not None
|
||||
}
|
||||
capabilities.update(
|
||||
{
|
||||
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(
|
||||
AvailableSource(
|
||||
id=source.id,
|
||||
enabled=source.enabled,
|
||||
capabilities=capabilities,
|
||||
)
|
||||
)
|
||||
return sources
|
||||
|
||||
|
||||
def _capability_name(qualified_name: str) -> str | None:
|
||||
"""Return the local name of one qualified capability ref if it is valid."""
|
||||
from wf_api.refs import parse_workflow_surface_capability_id
|
||||
from wf_artifacts import WorkflowCapabilityRef
|
||||
|
||||
try:
|
||||
parsed = parse_workflow_surface_capability_id(qualified_name)
|
||||
except ValueError:
|
||||
return None
|
||||
if isinstance(parsed, WorkflowCapabilityRef):
|
||||
return None
|
||||
return parsed.name
|
||||
|
||||
|
||||
def _deployment_summary(deployment: WorkflowDeployment) -> dict[str, Any]:
|
||||
"""Return compact deployment metadata for progressive list responses."""
|
||||
return {
|
||||
"id": deployment.id,
|
||||
"artifact_id": deployment.artifact_id,
|
||||
"artifact_version": deployment.artifact_version,
|
||||
"binding_count": len(deployment.binding_map()),
|
||||
"drift_policy": deployment.drift_policy.value,
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"WorkflowDeploymentApi",
|
||||
]
|
||||
@@ -1,15 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Protocol
|
||||
|
||||
from wf_artifacts import (
|
||||
DependencyDiagnostic,
|
||||
DraftWorkspaceStore,
|
||||
RunStore,
|
||||
WorkflowArtifact,
|
||||
WorkflowArtifactCatalogEntry,
|
||||
WorkflowArtifactStore,
|
||||
WorkflowDeployment,
|
||||
)
|
||||
from wf_authoring import AsyncRegistryHandler
|
||||
from wf_core import RunState
|
||||
@@ -23,7 +25,17 @@ class WorkflowEventRecorder(Protocol):
|
||||
"""Records workflow lifecycle events without exposing MCP event types."""
|
||||
|
||||
def record_event(self, event: object) -> None:
|
||||
"""Record one event object supplied by an adapter-owned event factory."""
|
||||
"""Record one adapter-native event object."""
|
||||
...
|
||||
|
||||
def record_workflow_event(
|
||||
self,
|
||||
event_type: str,
|
||||
*,
|
||||
capability_id: str,
|
||||
payload: dict[str, Any],
|
||||
) -> None:
|
||||
"""Record one workflow lifecycle event by protocol-neutral fields."""
|
||||
...
|
||||
|
||||
|
||||
@@ -85,8 +97,13 @@ class WorkflowRuntimeRunner(Protocol):
|
||||
class WorkflowLiveSourceChecker(Protocol):
|
||||
"""Optional hook for validating live external source availability."""
|
||||
|
||||
async def available_sources(self) -> list[object]:
|
||||
"""Return source availability records understood by the caller."""
|
||||
async def deployment_diagnostics(
|
||||
self,
|
||||
*,
|
||||
deployment: WorkflowDeployment,
|
||||
artifacts: Sequence[WorkflowArtifact],
|
||||
) -> list[DependencyDiagnostic]:
|
||||
"""Return opt-in live-source diagnostics for a deployment tree."""
|
||||
...
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user