feat: type artifact operation results

This commit is contained in:
lda
2026-07-30 22:31:00 +07:00 Verified
parent 93e690c4d0
commit eae6367d99
10 changed files with 323 additions and 85 deletions
+3 -3
View File
@@ -71,9 +71,9 @@
- A 2026-07-30 spike confirmed that `fastapi-jsonrpc` already exports a - A 2026-07-30 spike confirmed that `fastapi-jsonrpc` already exports a
complete OpenRPC document for all 70 registered methods. Request payloads complete OpenRPC document for all 70 registered methods. Request payloads
retain useful Pydantic schemas, so OpenRPC is a viable transport input. retain useful Pydantic schemas, so OpenRPC is a viable transport input.
- The first typed-result slice now gives `workflow.health` and all deployment - Typed-result slices now give `workflow.health` and all artifact, deployment,
and run operations named transport-neutral result schemas: 11 of 70 and run operations named transport-neutral result schemas: 16 of 70
methods. The remaining 59 success results still collapse to generic objects methods. The remaining 54 success results still collapse to generic objects
because their Python API and JSON-RPC handlers return `dict[str, Any]`. because their Python API and JSON-RPC handlers return `dict[str, Any]`.
Continue introducing operation result DTOs before adopting generated Continue introducing operation result DTOs before adopting generated
TypeScript contracts. TypeScript contracts.
+57 -31
View File
@@ -24,9 +24,21 @@ from .artifact_refs import artifact_capability_id
from .capability_requirements import observed_node_specs from .capability_requirements import observed_node_specs
from .drafts import WorkflowDraftApi from .drafts import WorkflowDraftApi
from .listing import matches_query, paged_list_payload from .listing import matches_query, paged_list_payload
from .models import RawWorkflowPlan from .models import (
DeleteArtifactResult,
JsonProjector,
ListArtifactsResult,
RawWorkflowPlan,
SaveArtifactResult,
WorkflowArtifactPayload,
)
from .operation_context import WorkflowOperationContext from .operation_context import WorkflowOperationContext
_PROJECT_ARTIFACT = JsonProjector(WorkflowArtifactPayload)
_PROJECT_ARTIFACT_LIST = JsonProjector(ListArtifactsResult)
_PROJECT_ARTIFACT_SAVE = JsonProjector(SaveArtifactResult)
_PROJECT_ARTIFACT_DELETE = JsonProjector(DeleteArtifactResult)
class WorkflowArtifactApi: class WorkflowArtifactApi:
"""Saved workflow artifact operations. """Saved workflow artifact operations.
@@ -51,14 +63,16 @@ class WorkflowArtifactApi:
kind: ArtifactKind | None = None, kind: ArtifactKind | None = None,
cursor: str | None = None, cursor: str | None = None,
limit: int = 50, limit: int = 50,
) -> dict[str, Any]: ) -> ListArtifactsResult:
"""Return compact paged saved artifact summaries. """Return compact paged saved artifact summaries.
Saved artifacts can contain full raw workflow plans, so list results Saved artifacts can contain full raw workflow plans, so list results
deliberately stay summary-only. Use inspect/run tools for detail. deliberately stay summary-only. Use inspect/run tools for detail.
""" """
if self.context.artifact_store is None: if self.context.artifact_store is None:
return paged_list_payload("nodes", [], cursor=cursor, limit=limit) return _PROJECT_ARTIFACT_LIST(
paged_list_payload("nodes", [], cursor=cursor, limit=limit)
)
entries = [ entries = [
artifact_catalog_entry(artifact).model_dump(mode="json") artifact_catalog_entry(artifact).model_dump(mode="json")
for artifact in self.context.artifact_store.list_artifacts() for artifact in self.context.artifact_store.list_artifacts()
@@ -77,9 +91,13 @@ class WorkflowArtifactApi:
) )
] ]
entries.sort(key=lambda entry: str(entry.get("name", ""))) entries.sort(key=lambda entry: str(entry.get("name", "")))
return paged_list_payload("nodes", entries, cursor=cursor, limit=limit) return _PROJECT_ARTIFACT_LIST(
paged_list_payload("nodes", entries, cursor=cursor, limit=limit)
)
async def save_artifact(self, artifact: dict[str, Any]) -> dict[str, Any]: async def save_artifact(
self, artifact: dict[str, Any]
) -> SaveArtifactResult:
workflow_artifact = WorkflowArtifact.model_validate(artifact) workflow_artifact = WorkflowArtifact.model_validate(artifact)
self._artifact_store().save_artifact(workflow_artifact) self._artifact_store().save_artifact(workflow_artifact)
self.context.events.record_workflow_event( self.context.events.record_workflow_event(
@@ -90,11 +108,13 @@ class WorkflowArtifactApi:
"version": workflow_artifact.version, "version": workflow_artifact.version,
}, },
) )
return { return _PROJECT_ARTIFACT_SAVE(
"artifact_id": workflow_artifact.id, {
"version": workflow_artifact.version, "artifact_id": workflow_artifact.id,
"saved": True, "version": workflow_artifact.version,
} "saved": True,
}
)
async def create_artifact_from_plan( async def create_artifact_from_plan(
self, self,
@@ -109,7 +129,7 @@ class WorkflowArtifactApi:
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, source_bindings: dict[str, str] | None = None,
created_from_catalog_version: str | None = None, created_from_catalog_version: str | None = None,
) -> dict[str, Any]: ) -> SaveArtifactResult:
typed_plan = ( typed_plan = (
plan plan
if isinstance(plan, RawWorkflowPlan) if isinstance(plan, RawWorkflowPlan)
@@ -141,11 +161,13 @@ class WorkflowArtifactApi:
"created_from_plan": True, "created_from_plan": True,
}, },
) )
return { return _PROJECT_ARTIFACT_SAVE(
"artifact_id": workflow_artifact.id, {
"version": workflow_artifact.version, "artifact_id": workflow_artifact.id,
"saved": True, "version": workflow_artifact.version,
} "saved": True,
}
)
async def create_artifact_from_draft( async def create_artifact_from_draft(
self, self,
@@ -274,35 +296,39 @@ class WorkflowArtifactApi:
async def inspect_artifact( async def inspect_artifact(
self, *, artifact_id: str, version: int self, *, artifact_id: str, version: int
) -> dict[str, Any]: ) -> WorkflowArtifactPayload:
artifact = self._artifact_store().get_artifact(artifact_id, version) artifact = self._artifact_store().get_artifact(artifact_id, version)
return artifact.model_dump(mode="json") return _PROJECT_ARTIFACT(artifact.model_dump(mode="json"))
async def delete_artifact( async def delete_artifact(
self, *, artifact_id: str, version: int self, *, artifact_id: str, version: int
) -> dict[str, Any]: ) -> DeleteArtifactResult:
store = self._artifact_store() store = self._artifact_store()
blockers = store.deployments_for_artifact(artifact_id, version) blockers = store.deployments_for_artifact(artifact_id, version)
blocker_ids = [deployment.id for deployment in blockers] blocker_ids = [deployment.id for deployment in blockers]
if blocker_ids: if blocker_ids:
return { return _PROJECT_ARTIFACT_DELETE(
"artifact_id": artifact_id, {
"version": version, "artifact_id": artifact_id,
"deleted": False, "version": version,
"blocked_by_deployments": blocker_ids, "deleted": False,
} "blocked_by_deployments": blocker_ids,
}
)
store.delete_artifact(artifact_id, version) store.delete_artifact(artifact_id, version)
self.context.events.record_workflow_event( self.context.events.record_workflow_event(
"workflow_artifact_deleted", "workflow_artifact_deleted",
capability_id=f"{artifact_id}@{version}", capability_id=f"{artifact_id}@{version}",
payload={"artifact_id": artifact_id, "version": version}, payload={"artifact_id": artifact_id, "version": version},
) )
return { return _PROJECT_ARTIFACT_DELETE(
"artifact_id": artifact_id, {
"version": version, "artifact_id": artifact_id,
"deleted": True, "version": version,
"blocked_by_deployments": [], "deleted": True,
} "blocked_by_deployments": [],
}
)
def _suggested_self_bindings( def _suggested_self_bindings(
+22
View File
@@ -1,5 +1,16 @@
"""Transport-neutral workflow API models.""" """Transport-neutral workflow API models."""
from .artifacts import (
ArtifactCatalogEntryPayload,
ArtifactKindPayload,
CapabilityKindPayload,
CapabilityRefPayload,
DeleteArtifactResult,
ListArtifactsResult,
RequiredCapabilityPayload,
SaveArtifactResult,
WorkflowArtifactPayload,
)
from .common import ( from .common import (
ArtifactVersionPayload, ArtifactVersionPayload,
DependencyDiagnosticPayload, DependencyDiagnosticPayload,
@@ -10,6 +21,7 @@ from .common import (
JsonSchema, JsonSchema,
NextActionPatchExamplePayload, NextActionPatchExamplePayload,
NextActionsPayload, NextActionsPayload,
PageMetadataPayload,
RawWorkflowPlan, RawWorkflowPlan,
TraceRange, TraceRange,
) )
@@ -37,6 +49,11 @@ from .runs import (
__all__ = [ __all__ = [
"ArtifactVersionPayload", "ArtifactVersionPayload",
"ArtifactCatalogEntryPayload",
"ArtifactKindPayload",
"CapabilityKindPayload",
"CapabilityRefPayload",
"DeleteArtifactResult",
"DeleteDeploymentResult", "DeleteDeploymentResult",
"DependencyDiagnosticPayload", "DependencyDiagnosticPayload",
"DeploymentSummary", "DeploymentSummary",
@@ -48,20 +65,25 @@ __all__ = [
"JsonProjector", "JsonProjector",
"JsonSchema", "JsonSchema",
"ListDeploymentsResult", "ListDeploymentsResult",
"ListArtifactsResult",
"ListRunsResult", "ListRunsResult",
"NextActionPatchExamplePayload", "NextActionPatchExamplePayload",
"NextActionsPayload", "NextActionsPayload",
"PageMetadataPayload",
"RawWorkflowPlan", "RawWorkflowPlan",
"ResumeReadiness", "ResumeReadiness",
"RunResult", "RunResult",
"RunStatus", "RunStatus",
"RunSummary", "RunSummary",
"RunTraceResult", "RunTraceResult",
"RequiredCapabilityPayload",
"SaveArtifactResult",
"SaveDeploymentResult", "SaveDeploymentResult",
"SourceBindingPayload", "SourceBindingPayload",
"TraceRange", "TraceRange",
"TraceEntryPayload", "TraceEntryPayload",
"ValidateDeploymentResult", "ValidateDeploymentResult",
"WorkflowDeploymentPayload", "WorkflowDeploymentPayload",
"WorkflowArtifactPayload",
"WorkflowRefPayload", "WorkflowRefPayload",
] ]
+87
View File
@@ -0,0 +1,87 @@
from __future__ import annotations
from typing import Literal, TypedDict
from .common import (
DependencyDiagnosticPayload,
JsonObject,
PageMetadataPayload,
)
type ArtifactKindPayload = Literal["workflow", "wrapper"]
type CapabilityKindPayload = Literal[
"tool",
"resource",
"prompt",
"node_spec",
"reducer",
"workflow",
]
class CapabilityRefPayload(TypedDict):
source: str
capability_key: str
class RequiredCapabilityPayload(TypedDict):
"""Saved dependency contract for one artifact capability reference."""
ref: CapabilityRefPayload
kind: CapabilityKindPayload
input_schema_hash: str | None
input_schema_snapshot: JsonObject | None
output_schema_hash: str | None
output_schema_snapshot: JsonObject | None
observed_concrete_source: str | None
observed_at_epoch_ms: int | None
class ArtifactCatalogEntryPayload(TypedDict):
"""Compact artifact row returned by discovery operations."""
name: str
artifact_id: str
version: int
kind: str
display_name: str
description: str | None
outcomes: list[str]
input_schema: JsonObject
output_schema: JsonObject
required_sources: list[str]
diagnostics: list[DependencyDiagnosticPayload]
class ListArtifactsResult(PageMetadataPayload):
nodes: list[ArtifactCatalogEntryPayload]
class WorkflowArtifactPayload(TypedDict):
"""Normalized immutable artifact returned by inspect operations."""
id: str
version: int
title: str
kind: ArtifactKindPayload
description: str | None
input_schema: JsonObject
output_schema: JsonObject
outcomes: list[str]
plan: JsonObject
required_capabilities: list[RequiredCapabilityPayload]
workflow_dependencies: dict[str, int]
created_from_catalog_version: str | None
class SaveArtifactResult(TypedDict):
artifact_id: str
version: int
saved: bool
class DeleteArtifactResult(TypedDict):
artifact_id: str
version: int
deleted: bool
blocked_by_deployments: list[str]
+7
View File
@@ -73,6 +73,13 @@ class GuidedResultPayload(TypedDict):
next_actions: NextActionsPayload next_actions: NextActionsPayload
class PageMetadataPayload(TypedDict):
"""Cursor metadata shared by compact workflow discovery responses."""
next_cursor: str | None
total: int
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class TraceRange: class TraceRange:
"""Caller-bounded debug trace slice for durable deployment runs.""" """Caller-bounded debug trace slice for durable deployment runs."""
+9 -5
View File
@@ -14,14 +14,18 @@ from .draft_authoring import RouteSource, WorkflowDraftAuthoringApi
from .draft_updates import CapabilityStepUpdate from .draft_updates import CapabilityStepUpdate
from .drafts import WorkflowDraftApi from .drafts import WorkflowDraftApi
from .models import ( from .models import (
DeleteArtifactResult,
DeleteDeploymentResult, DeleteDeploymentResult,
ListArtifactsResult,
ListDeploymentsResult, ListDeploymentsResult,
ListRunsResult, ListRunsResult,
RawWorkflowPlan, RawWorkflowPlan,
RunResult, RunResult,
RunTraceResult, RunTraceResult,
SaveArtifactResult,
SaveDeploymentResult, SaveDeploymentResult,
ValidateDeploymentResult, ValidateDeploymentResult,
WorkflowArtifactPayload,
WorkflowDeploymentPayload, WorkflowDeploymentPayload,
) )
from .operation_context import WorkflowOperationContext from .operation_context import WorkflowOperationContext
@@ -91,7 +95,7 @@ class WorkflowApi:
kind: ArtifactKind | None = None, kind: ArtifactKind | None = None,
cursor: str | None = None, cursor: str | None = None,
limit: int = 50, limit: int = 50,
) -> dict[str, Any]: ) -> ListArtifactsResult:
return await self.artifacts.list_artifacts( return await self.artifacts.list_artifacts(
query=query, query=query,
kind=kind, kind=kind,
@@ -104,7 +108,7 @@ class WorkflowApi:
*, *,
artifact_id: str, artifact_id: str,
version: int, version: int,
) -> dict[str, Any]: ) -> WorkflowArtifactPayload:
return await self.artifacts.inspect_artifact( return await self.artifacts.inspect_artifact(
artifact_id=artifact_id, artifact_id=artifact_id,
version=version, version=version,
@@ -115,7 +119,7 @@ class WorkflowApi:
*, *,
artifact_id: str, artifact_id: str,
version: int, version: int,
) -> dict[str, Any]: ) -> DeleteArtifactResult:
return await self.artifacts.delete_artifact( return await self.artifacts.delete_artifact(
artifact_id=artifact_id, artifact_id=artifact_id,
version=version, version=version,
@@ -124,7 +128,7 @@ class WorkflowApi:
async def save_artifact( async def save_artifact(
self, self,
artifact: dict[str, Any], artifact: dict[str, Any],
) -> dict[str, Any]: ) -> SaveArtifactResult:
return await self.artifacts.save_artifact(artifact) return await self.artifacts.save_artifact(artifact)
async def create_artifact_from_plan( async def create_artifact_from_plan(
@@ -140,7 +144,7 @@ class WorkflowApi:
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, source_bindings: dict[str, str] | None = None,
created_from_catalog_version: str | None = None, created_from_catalog_version: str | None = None,
) -> dict[str, Any]: ) -> SaveArtifactResult:
return await self.artifacts.create_artifact_from_plan( return await self.artifacts.create_artifact_from_plan(
artifact_id=artifact_id, artifact_id=artifact_id,
version=version, version=version,
+13 -4
View File
@@ -10,13 +10,17 @@ from wf_core.models.steps import InputBinding, OutputBinding
from .draft_authoring import RouteSource from .draft_authoring import RouteSource
from .draft_updates import CapabilityStepUpdate from .draft_updates import CapabilityStepUpdate
from .models import ( from .models import (
DeleteArtifactResult,
DeleteDeploymentResult, DeleteDeploymentResult,
ListArtifactsResult,
ListDeploymentsResult, ListDeploymentsResult,
ListRunsResult, ListRunsResult,
RunResult, RunResult,
RunTraceResult, RunTraceResult,
SaveArtifactResult,
SaveDeploymentResult, SaveDeploymentResult,
ValidateDeploymentResult, ValidateDeploymentResult,
WorkflowArtifactPayload,
WorkflowDeploymentPayload, WorkflowDeploymentPayload,
) )
from .runs import TraceRangeLike from .runs import TraceRangeLike
@@ -355,21 +359,26 @@ class WorkflowArtifactSurface(Protocol):
kind: ArtifactKind | None = None, kind: ArtifactKind | None = None,
cursor: str | None = None, cursor: str | None = None,
limit: int = 50, limit: int = 50,
) -> dict[str, Any]: ... ) -> ListArtifactsResult: ...
async def inspect_artifact( async def inspect_artifact(
self, self,
*, *,
artifact_id: str, artifact_id: str,
version: int, version: int,
) -> dict[str, Any]: ... ) -> WorkflowArtifactPayload: ...
async def save_artifact(
self,
artifact: dict[str, Any],
) -> SaveArtifactResult: ...
async def delete_artifact( async def delete_artifact(
self, self,
*, *,
artifact_id: str, artifact_id: str,
version: int, version: int,
) -> dict[str, Any]: ... ) -> DeleteArtifactResult: ...
async def create_artifact_from_plan( async def create_artifact_from_plan(
self, self,
@@ -384,7 +393,7 @@ class WorkflowArtifactSurface(Protocol):
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, source_bindings: dict[str, str] | None = None,
created_from_catalog_version: str | None = None, created_from_catalog_version: str | None = None,
) -> dict[str, Any]: ... ) -> SaveArtifactResult: ...
class WorkflowDeploymentSurface(Protocol): class WorkflowDeploymentSurface(Protocol):
+57 -35
View File
@@ -1,7 +1,14 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Literal from typing import Any, Literal, cast
from wf_api.models import (
DeleteArtifactResult,
ListArtifactsResult,
SaveArtifactResult,
WorkflowArtifactPayload,
)
from .base import RpcCaller from .base import RpcCaller
@@ -16,36 +23,48 @@ class RpcArtifactClientMixin:
kind: Literal["workflow", "wrapper"] | None = None, kind: Literal["workflow", "wrapper"] | None = None,
cursor: str | None = None, cursor: str | None = None,
limit: int = 50, limit: int = 50,
) -> dict[str, Any]: ) -> ListArtifactsResult:
return await self._call( return cast(
"workflow.artifacts.list", ListArtifactsResult,
{ await self._call(
"query": query, "workflow.artifacts.list",
"kind": kind, {
"cursor": cursor, "query": query,
"limit": limit, "kind": kind,
}, "cursor": cursor,
"limit": limit,
},
),
) )
async def inspect_artifact( async def inspect_artifact(
self: RpcCaller, *, artifact_id: str, version: int self: RpcCaller, *, artifact_id: str, version: int
) -> dict[str, Any]: ) -> WorkflowArtifactPayload:
return await self._call( return cast(
"workflow.artifacts.inspect", WorkflowArtifactPayload,
{"artifact_id": artifact_id, "version": version}, await self._call(
"workflow.artifacts.inspect",
{"artifact_id": artifact_id, "version": version},
),
) )
async def save_artifact( async def save_artifact(
self: RpcCaller, artifact: dict[str, Any] self: RpcCaller, artifact: dict[str, Any]
) -> dict[str, Any]: ) -> SaveArtifactResult:
return await self._call("workflow.artifacts.save", {"artifact": artifact}) return cast(
SaveArtifactResult,
await self._call("workflow.artifacts.save", {"artifact": artifact}),
)
async def delete_artifact( async def delete_artifact(
self: RpcCaller, *, artifact_id: str, version: int self: RpcCaller, *, artifact_id: str, version: int
) -> dict[str, Any]: ) -> DeleteArtifactResult:
return await self._call( return cast(
"workflow.artifacts.delete", DeleteArtifactResult,
{"artifact_id": artifact_id, "version": version}, await self._call(
"workflow.artifacts.delete",
{"artifact_id": artifact_id, "version": version},
),
) )
async def create_artifact_from_plan( async def create_artifact_from_plan(
@@ -61,19 +80,22 @@ class RpcArtifactClientMixin:
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, source_bindings: dict[str, str] | None = None,
created_from_catalog_version: str | None = None, created_from_catalog_version: str | None = None,
) -> dict[str, Any]: ) -> SaveArtifactResult:
return await self._call( return cast(
"workflow.artifacts.create_from_plan", SaveArtifactResult,
{ await self._call(
"artifact_id": artifact_id, "workflow.artifacts.create_from_plan",
"version": version, {
"title": title, "artifact_id": artifact_id,
"plan": plan, "version": version,
"outcomes": list(outcomes), "title": title,
"kind": kind, "plan": plan,
"description": description, "outcomes": list(outcomes),
"required_capabilities": required_capabilities, "kind": kind,
"source_bindings": source_bindings, "description": description,
"created_from_catalog_version": created_from_catalog_version, "required_capabilities": required_capabilities,
}, "source_bindings": source_bindings,
"created_from_catalog_version": created_from_catalog_version,
},
),
) )
+15 -7
View File
@@ -1,9 +1,17 @@
from __future__ import annotations """Artifact JSON-RPC method registration.
from typing import Any Return annotations stay eagerly evaluated because fastapi-jsonrpc captures them
while registering nested handlers for response validation and OpenRPC output.
"""
import fastapi_jsonrpc as jsonrpc import fastapi_jsonrpc as jsonrpc
from wf_api.models import (
DeleteArtifactResult,
ListArtifactsResult,
SaveArtifactResult,
WorkflowArtifactPayload,
)
from wf_server import WorkflowServer from wf_server import WorkflowServer
from ..errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
@@ -29,7 +37,7 @@ def register_methods(
) )
async def workflow_artifacts_create_from_plan( async def workflow_artifacts_create_from_plan(
params: CreateArtifactFromPlanParams = RpcParams(), params: CreateArtifactFromPlanParams = RpcParams(),
) -> dict[str, Any]: ) -> SaveArtifactResult:
try: try:
return await server.api.create_artifact_from_plan( return await server.api.create_artifact_from_plan(
artifact_id=params.artifact_id, artifact_id=params.artifact_id,
@@ -49,7 +57,7 @@ def register_methods(
@entrypoint.method(name="workflow.artifacts.save", errors=[WorkflowRpcError]) @entrypoint.method(name="workflow.artifacts.save", errors=[WorkflowRpcError])
async def workflow_artifacts_save( async def workflow_artifacts_save(
params: SaveArtifactParams = RpcParams(), params: SaveArtifactParams = RpcParams(),
) -> dict[str, Any]: ) -> SaveArtifactResult:
try: try:
return await server.api.save_artifact(params.artifact) return await server.api.save_artifact(params.artifact)
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc: except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
@@ -58,7 +66,7 @@ def register_methods(
@entrypoint.method(name="workflow.artifacts.list", errors=[WorkflowRpcError]) @entrypoint.method(name="workflow.artifacts.list", errors=[WorkflowRpcError])
async def workflow_artifacts_list( async def workflow_artifacts_list(
params: ListArtifactsParams = RpcParams(), params: ListArtifactsParams = RpcParams(),
) -> dict[str, Any]: ) -> ListArtifactsResult:
try: try:
return await server.api.list_artifacts( return await server.api.list_artifacts(
query=params.query, query=params.query,
@@ -72,7 +80,7 @@ def register_methods(
@entrypoint.method(name="workflow.artifacts.inspect", errors=[WorkflowRpcError]) @entrypoint.method(name="workflow.artifacts.inspect", errors=[WorkflowRpcError])
async def workflow_artifacts_inspect( async def workflow_artifacts_inspect(
params: InspectArtifactParams = RpcParams(), params: InspectArtifactParams = RpcParams(),
) -> dict[str, Any]: ) -> WorkflowArtifactPayload:
try: try:
return await server.api.inspect_artifact( return await server.api.inspect_artifact(
artifact_id=params.artifact_id, artifact_id=params.artifact_id,
@@ -84,7 +92,7 @@ def register_methods(
@entrypoint.method(name="workflow.artifacts.delete", errors=[WorkflowRpcError]) @entrypoint.method(name="workflow.artifacts.delete", errors=[WorkflowRpcError])
async def workflow_artifacts_delete( async def workflow_artifacts_delete(
params: DeleteArtifactParams = RpcParams(), params: DeleteArtifactParams = RpcParams(),
) -> dict[str, Any]: ) -> DeleteArtifactResult:
try: try:
return await server.api.delete_artifact( return await server.api.delete_artifact(
artifact_id=params.artifact_id, artifact_id=params.artifact_id,
@@ -46,6 +46,59 @@ def test_openrpc_exposes_typed_health_result(
) )
@pytest.mark.parametrize(
("method_name", "component_name", "properties"),
[
(
"workflow.artifacts.create_from_plan",
"SaveArtifactResult",
{"artifact_id", "version", "saved"},
),
(
"workflow.artifacts.save",
"SaveArtifactResult",
{"artifact_id", "version", "saved"},
),
(
"workflow.artifacts.list",
"ListArtifactsResult",
{"nodes", "next_cursor", "total"},
),
(
"workflow.artifacts.inspect",
"WorkflowArtifactPayload",
{
"id",
"version",
"title",
"kind",
"input_schema",
"output_schema",
"outcomes",
"plan",
},
),
(
"workflow.artifacts.delete",
"DeleteArtifactResult",
{"artifact_id", "version", "deleted", "blocked_by_deployments"},
),
],
)
def test_openrpc_exposes_typed_artifact_results(
openrpc_document: dict[str, Any],
method_name: str,
component_name: str,
properties: set[str],
) -> None:
_assert_result_component(
openrpc_document,
method_name=method_name,
component_name=component_name,
properties=properties,
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
("method_name", "component_name", "properties"), ("method_name", "component_name", "properties"),
[ [