misc: use structured path for errors

This commit is contained in:
lda
2026-05-28 23:25:10 +07:00 Verified
parent ffd352b660
commit 4bad1c4e2c
18 changed files with 617 additions and 35 deletions
+12 -6
View File
@@ -69,7 +69,10 @@ from .run_lifecycle import (
restore_interrupted_run,
validate_pinned_resume_environment,
)
from .wrapper_hints import wrapper_hints_for_capability
from .wrapper_hints import (
workflow_output_schema_for_authoring,
wrapper_hints_for_capability,
)
if TYPE_CHECKING:
from wf_core import RunState
@@ -138,7 +141,9 @@ class WorkflowSurfaceHandlers:
"outcomes": list(detail.outcomes),
"is_async": detail.is_async,
"input_fields": _schema_field_names(detail.input_schema),
"output_fields": _schema_field_names(detail.output_schema),
"output_fields": _schema_field_names(
workflow_output_schema_for_authoring(detail.output_schema)
),
}
for source in sorted(
self.service.capability_sources.values(),
@@ -716,7 +721,7 @@ class WorkflowSurfaceHandlers:
output: Sequence[OutputBinding] | None = None,
input_map: dict[str, str] | None = None,
output_map: dict[str, str] | None = None,
error_message_source: str | None = None,
error_message_source: str | GraphSourcePath | None = None,
title: str | None = None,
) -> dict[str, Any]:
"""Bootstrap the smallest patchable draft around one workflow capability."""
@@ -787,7 +792,7 @@ class WorkflowSurfaceHandlers:
output: Sequence[OutputBinding] | None = None,
input_map: dict[str, str] | None = None,
output_map: dict[str, str] | None = None,
error_message_source: str | None = None,
error_message_source: str | GraphSourcePath | None = None,
) -> dict[str, Any]:
"""Create a patchable draft workspace from inspect_capability hints."""
capability = await self.inspect_capability(qualified_name=capability_name)
@@ -1339,8 +1344,9 @@ def _draft_output_bindings_payload(output_map: dict[str, str]) -> list[dict[str,
]
def _graph_path_payload(value: str) -> dict[str, str | list[str]]:
return GraphSourcePath._serialize(GraphSourcePath.parse(value))
def _graph_path_payload(value: str | GraphSourcePath) -> dict[str, str | list[str]]:
path = value if isinstance(value, GraphSourcePath) else GraphSourcePath.parse(value)
return GraphSourcePath._serialize(path)
def _local_path_payload(value: str) -> dict[str, str | list[str]]:
+13 -2
View File
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from wf_artifacts import ArtifactKind
from wf_artifacts.draft_workspaces.models import WORKSPACE_ID_PATTERN
from wf_core.models.steps import InputBinding, OutputBinding
from wf_core.paths import GraphSourcePath
WorkspaceId = Annotated[
str,
@@ -67,6 +68,16 @@ SourceBindings = Annotated[
)
),
]
ErrorMessageSource = Annotated[
GraphSourcePath | str,
Field(
description=(
"State path for runtime_error.message. Prefer structural paths such "
"as {'root': 'state', 'parts': ['error_message']}; strings like "
"state.error_message remain compatibility input."
)
),
]
class CallCapabilityResult(BaseModel):
@@ -276,7 +287,7 @@ class CreateMinimalDraftWorkspaceRequest(BaseModel):
"workflow state paths, for example {'echoed': 'state.echoed'}."
),
)
error_message_source: str | None = Field(
error_message_source: ErrorMessageSource | None = Field(
default=None,
description=(
"Optional state path used as runtime_error.message when the capability "
@@ -332,7 +343,7 @@ class CreateDraftWorkspaceFromCapabilityRequest(BaseModel):
"Deprecated compatibility override for the hinted capability output map."
),
)
error_message_source: str | None = Field(
error_message_source: ErrorMessageSource | None = Field(
default=None,
description=(
"Optional state path used as runtime_error.message when the capability "
+60 -13
View File
@@ -103,22 +103,26 @@ def wrapper_hints_for_capability(
create routes by itself.
"""
input_properties = _object_properties(input_schema)
output_properties = _object_properties(output_schema)
hint_output_schema = workflow_output_schema_for_authoring(output_schema)
output_properties = _object_properties(hint_output_schema)
input_map = {f"input.{name}": name for name in sorted(input_properties)}
output_map = {name: f"state.{name}" for name in sorted(output_properties)}
output_map_properties = _default_output_map_properties(
output_schema, output_properties
)
output_map = {name: f"state.{name}" for name in sorted(output_map_properties)}
state_schema = {
"type": "object",
"properties": {
name: schema for name, schema in sorted(output_properties.items())
name: schema for name, schema in sorted(output_map_properties.items())
},
}
wrapper_output_schema = {
"type": "object",
"properties": {
name: schema for name, schema in sorted(output_properties.items())
name: schema for name, schema in sorted(output_map_properties.items())
},
}
missing_decisions = _missing_decisions_for_output(output_schema)
missing_decisions = _missing_decisions_for_output(hint_output_schema)
outcome_candidates = _boolean_outcome_candidates(output_properties)
if outcome_candidates:
missing_decisions.append(
@@ -132,10 +136,23 @@ def wrapper_hints_for_capability(
)
confidence = _confidence_for_hint(
input_schema=input_schema,
output_schema=output_schema,
output_schema=hint_output_schema,
missing_decisions=missing_decisions,
outcome_candidates=outcome_candidates,
)
notes = [
"Hints are scaffolding, not semantic guarantees.",
(
"Declared outcomes are preserved; output-field outcome "
"inference is not automatic."
),
]
if _has_raw_mcp_content(output_schema):
notes.append(
"Raw MCP content blocks are not workflow-shaped. Use an explicit "
"wrapper or extraction node to handle TextContent, ResourceLink, "
"images, or mixed content before writing to typed state."
)
return WrapperAuthoringHints(
capability_name=capability_name,
confidence=confidence,
@@ -149,16 +166,20 @@ def wrapper_hints_for_capability(
output_map=output_map,
outcome_candidates=outcome_candidates,
missing_decisions=missing_decisions,
notes=[
"Hints are scaffolding, not semantic guarantees.",
(
"Declared outcomes are preserved; output-field outcome "
"inference is not automatic."
),
],
notes=notes,
)
def workflow_output_schema_for_authoring(output_schema: JsonObject) -> JsonObject:
"""Return the workflow-author-facing output schema for one capability.
Raw MCP ``content`` blocks stay raw. A wrapper may choose to extract
``content[0].text`` or handle resources/images, but the authoring surface
must not invent that decision as a top-level schema field.
"""
return {"type": "object", "properties": _object_properties(output_schema)}
def _object_properties(schema: JsonObject) -> dict[str, JsonObject]:
"""Return object properties that are themselves JSON Schema objects."""
properties = schema.get("properties")
@@ -171,6 +192,32 @@ def _object_properties(schema: JsonObject) -> dict[str, JsonObject]:
}
def _has_raw_mcp_content(schema: JsonObject) -> bool:
"""Return true when schema exposes MCP's raw content-block envelope."""
properties = _object_properties(schema)
content_schema = properties.get("content")
return isinstance(content_schema, dict) and content_schema.get("type") == "array"
def _default_output_map_properties(
raw_output_schema: JsonObject,
output_properties: dict[str, JsonObject],
) -> dict[str, JsonObject]:
"""Return fields safe enough to wire by default in generated hints.
Raw MCP ``content`` is a protocol envelope, not a workflow value. Keeping it
out of the default map prevents the scaffold from writing text/image/resource
blocks into typed state without an explicit extraction wrapper.
"""
if _has_raw_mcp_content(raw_output_schema):
return {
name: schema
for name, schema in output_properties.items()
if name != "content"
}
return output_properties
def _missing_decisions_for_output(output_schema: JsonObject) -> list[MissingDecision]:
"""Return explicit decisions required by output schema shape."""
properties = _object_properties(output_schema)