misc: use structured path for errors
This commit is contained in:
@@ -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]]:
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user