822 lines
26 KiB
Markdown
822 lines
26 KiB
Markdown
# Wrapper Authoring Hints Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Add enum-backed wrapper authoring hints to workflow capability inspection so LLM and human MCP clients can scaffold wrapper drafts without guessing raw workflow plans.
|
|
|
|
**Architecture:** Keep MCP request/response names as plain JSON strings, but compute wrapper hints in a pure workflow-surface helper. The helper uses typed Pydantic models with enum fields for confidence, outcome policy, candidate kinds, and missing-decision kinds, then `inspect_capability` attaches the hint payload to live NodeSpec and saved wrapper details.
|
|
|
|
**Tech Stack:** Python 3.14, Pydantic v2, FastMCP-facing dict payloads, pytest, ruff, basedpyright.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- Create `src/wf_mcp/workflow_surface/wrapper_hints.py`
|
|
- Owns enum-backed hint models.
|
|
- Owns pure functions that derive hints from one capability contract.
|
|
- Must not call MCP, stores, workflow runtime, or FastMCP.
|
|
- Modify `src/wf_mcp/workflow_surface/handlers.py`
|
|
- Imports the pure hint helper.
|
|
- Adds `wrapper_hints` to `inspect_capability` payloads.
|
|
- Modify `src/wf_mcp/workflow_surface/models.py`
|
|
- Adds JSON-schema-visible response models if MCP tool schemas need stronger documentation.
|
|
- Do this only after the helper shape stabilizes.
|
|
- Test `tests/wf_mcp/test_workflow_wrapper_hints.py`
|
|
- Focused unit tests for hint derivation.
|
|
- Test `tests/wf_mcp/test_workflow_surface.py`
|
|
- Integration tests proving `inspect_capability` includes hints.
|
|
- Docs `docs/workflow_capabilities.md`
|
|
- Explain that hints are scaffolding, not semantic guarantees.
|
|
|
|
## Vocabulary
|
|
|
|
Use enums for all classification/type fields. Do not return magic strings for these fields from helper internals.
|
|
|
|
```python
|
|
from enum import StrEnum
|
|
|
|
|
|
class WrapperHintConfidence(StrEnum):
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
class WrapperOutcomePolicy(StrEnum):
|
|
PRESERVE_DECLARED = "preserve_declared"
|
|
MANUAL_MAPPING_REQUIRED = "manual_mapping_required"
|
|
|
|
|
|
class OutcomeCandidateKind(StrEnum):
|
|
BOOLEAN_CONTROL_FIELD = "boolean_control_field"
|
|
|
|
|
|
class MissingDecisionKind(StrEnum):
|
|
CHOOSE_OUTPUT_FIELDS = "choose_output_fields"
|
|
REVIEW_NESTED_OUTPUT = "review_nested_output"
|
|
CONFIRM_BOOLEAN_OUTCOMES = "confirm_boolean_outcomes"
|
|
CHOOSE_ERROR_MAPPING = "choose_error_mapping"
|
|
```
|
|
|
|
MCP JSON still serializes those enum values as strings.
|
|
|
|
## Task 1: Add Enum-Backed Hint Models
|
|
|
|
**Files:**
|
|
|
|
- Create: `src/wf_mcp/workflow_surface/wrapper_hints.py`
|
|
- Test: `tests/wf_mcp/test_workflow_wrapper_hints.py`
|
|
|
|
- [ ] **Step 1: Write failing model serialization test**
|
|
|
|
Create `tests/wf_mcp/test_workflow_wrapper_hints.py`:
|
|
|
|
```python
|
|
from __future__ import annotations
|
|
|
|
from wf_mcp.workflow_surface.wrapper_hints import (
|
|
MissingDecision,
|
|
MissingDecisionKind,
|
|
OutcomeCandidate,
|
|
OutcomeCandidateKind,
|
|
WrapperAuthoringHints,
|
|
WrapperHintConfidence,
|
|
WrapperOutcomePolicy,
|
|
)
|
|
|
|
|
|
def test_wrapper_hint_models_serialize_enum_fields_as_strings() -> None:
|
|
hints = WrapperAuthoringHints(
|
|
capability_name="demo.personal.echo_tool",
|
|
confidence=WrapperHintConfidence.MEDIUM,
|
|
declared_outcomes=["ok", "error"],
|
|
suggested_wrapper_outcomes=["ok", "error"],
|
|
outcome_policy=WrapperOutcomePolicy.PRESERVE_DECLARED,
|
|
input_schema={"type": "object", "properties": {}},
|
|
state_schema={"type": "object", "properties": {}},
|
|
output_schema={"type": "object", "properties": {}},
|
|
input_map={},
|
|
output_map={},
|
|
outcome_candidates=[
|
|
OutcomeCandidate(
|
|
kind=OutcomeCandidateKind.BOOLEAN_CONTROL_FIELD,
|
|
source="output.success",
|
|
candidate_outcomes=["success", "failure"],
|
|
confidence=WrapperHintConfidence.MEDIUM,
|
|
reason="top-level boolean field with control-like name",
|
|
automatic=False,
|
|
)
|
|
],
|
|
missing_decisions=[
|
|
MissingDecision(
|
|
kind=MissingDecisionKind.CONFIRM_BOOLEAN_OUTCOMES,
|
|
message="Confirm whether output.success should control routing.",
|
|
)
|
|
],
|
|
notes=["Hints are scaffolding, not semantic guarantees."],
|
|
)
|
|
|
|
dumped = hints.model_dump(mode="json")
|
|
|
|
assert dumped["confidence"] == "medium"
|
|
assert dumped["outcome_policy"] == "preserve_declared"
|
|
assert dumped["outcome_candidates"][0]["kind"] == "boolean_control_field"
|
|
assert dumped["missing_decisions"][0]["kind"] == "confirm_boolean_outcomes"
|
|
```
|
|
|
|
- [ ] **Step 2: Run model test to verify it fails**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_wrapper_hints.py::test_wrapper_hint_models_serialize_enum_fields_as_strings -q
|
|
```
|
|
|
|
Expected: import failure because `wf_mcp.workflow_surface.wrapper_hints` does not exist.
|
|
|
|
- [ ] **Step 3: Implement model definitions**
|
|
|
|
Create `src/wf_mcp/workflow_surface/wrapper_hints.py`:
|
|
|
|
```python
|
|
from __future__ import annotations
|
|
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
JsonObject = dict[str, Any]
|
|
|
|
|
|
class WrapperHintConfidence(StrEnum):
|
|
"""Coarse confidence for generated wrapper scaffolding hints."""
|
|
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
class WrapperOutcomePolicy(StrEnum):
|
|
"""How wrapper outcomes were chosen."""
|
|
|
|
PRESERVE_DECLARED = "preserve_declared"
|
|
MANUAL_MAPPING_REQUIRED = "manual_mapping_required"
|
|
|
|
|
|
class OutcomeCandidateKind(StrEnum):
|
|
"""Reason a field was offered as a possible outcome source."""
|
|
|
|
BOOLEAN_CONTROL_FIELD = "boolean_control_field"
|
|
|
|
|
|
class MissingDecisionKind(StrEnum):
|
|
"""Typed action item a human or LLM must decide before saving a wrapper."""
|
|
|
|
CHOOSE_OUTPUT_FIELDS = "choose_output_fields"
|
|
REVIEW_NESTED_OUTPUT = "review_nested_output"
|
|
CONFIRM_BOOLEAN_OUTCOMES = "confirm_boolean_outcomes"
|
|
CHOOSE_ERROR_MAPPING = "choose_error_mapping"
|
|
|
|
|
|
class OutcomeCandidate(BaseModel):
|
|
"""One possible outcome mapping that must not be applied automatically."""
|
|
|
|
kind: OutcomeCandidateKind
|
|
source: str = Field(description="Output path such as output.success.")
|
|
candidate_outcomes: list[str]
|
|
confidence: WrapperHintConfidence
|
|
reason: str
|
|
automatic: bool = False
|
|
|
|
|
|
class MissingDecision(BaseModel):
|
|
"""One explicit decision required before a wrapper should be saved."""
|
|
|
|
kind: MissingDecisionKind
|
|
message: str
|
|
|
|
|
|
class WrapperAuthoringHints(BaseModel):
|
|
"""Scaffold for creating a workflow wrapper around one capability."""
|
|
|
|
capability_name: str
|
|
confidence: WrapperHintConfidence
|
|
declared_outcomes: list[str]
|
|
suggested_wrapper_outcomes: list[str]
|
|
outcome_policy: WrapperOutcomePolicy
|
|
input_schema: JsonObject
|
|
state_schema: JsonObject
|
|
output_schema: JsonObject
|
|
input_map: dict[str, str]
|
|
output_map: dict[str, str]
|
|
outcome_candidates: list[OutcomeCandidate] = Field(default_factory=list)
|
|
missing_decisions: list[MissingDecision] = Field(default_factory=list)
|
|
notes: list[str] = Field(default_factory=list)
|
|
```
|
|
|
|
- [ ] **Step 4: Run model test to verify it passes**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_wrapper_hints.py::test_wrapper_hint_models_serialize_enum_fields_as_strings -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
## Task 2: Derive Simple Wrapper Hints From Capability Schemas
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_mcp/workflow_surface/wrapper_hints.py`
|
|
- Test: `tests/wf_mcp/test_workflow_wrapper_hints.py`
|
|
|
|
- [ ] **Step 1: Add failing simple-schema hint test**
|
|
|
|
Append to `tests/wf_mcp/test_workflow_wrapper_hints.py`:
|
|
|
|
```python
|
|
from wf_mcp.workflow_surface.wrapper_hints import wrapper_hints_for_capability
|
|
|
|
|
|
def test_wrapper_hints_scaffold_simple_object_input_and_output() -> None:
|
|
hints = wrapper_hints_for_capability(
|
|
capability_name="demo.personal.echo_tool",
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {"text": {"type": "string"}},
|
|
"required": ["text"],
|
|
},
|
|
output_schema={
|
|
"type": "object",
|
|
"properties": {"echoed": {"type": "string"}},
|
|
"required": ["echoed"],
|
|
},
|
|
outcomes=["ok"],
|
|
)
|
|
|
|
dumped = hints.model_dump(mode="json")
|
|
|
|
assert dumped["confidence"] == "high"
|
|
assert dumped["declared_outcomes"] == ["ok"]
|
|
assert dumped["suggested_wrapper_outcomes"] == ["ok"]
|
|
assert dumped["outcome_policy"] == "preserve_declared"
|
|
assert dumped["input_map"] == {"input.text": "text"}
|
|
assert dumped["output_map"] == {"echoed": "state.echoed"}
|
|
assert dumped["state_schema"]["properties"]["echoed"]["type"] == "string"
|
|
assert dumped["output_schema"]["properties"]["echoed"]["type"] == "string"
|
|
assert dumped["missing_decisions"] == []
|
|
```
|
|
|
|
- [ ] **Step 2: Run simple-schema test to verify it fails**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_wrapper_hints.py::test_wrapper_hints_scaffold_simple_object_input_and_output -q
|
|
```
|
|
|
|
Expected: import failure or attribute error for `wrapper_hints_for_capability`.
|
|
|
|
- [ ] **Step 3: Implement `wrapper_hints_for_capability`**
|
|
|
|
Add to `src/wf_mcp/workflow_surface/wrapper_hints.py`:
|
|
|
|
```python
|
|
CONTROL_BOOLEAN_NAMES = {
|
|
"success",
|
|
"ok",
|
|
"failed",
|
|
"error",
|
|
"is_error",
|
|
"needs_input",
|
|
"requires_approval",
|
|
"approved",
|
|
"rejected",
|
|
"has_more",
|
|
"done",
|
|
"complete",
|
|
}
|
|
|
|
|
|
def wrapper_hints_for_capability(
|
|
*,
|
|
capability_name: str,
|
|
input_schema: JsonObject,
|
|
output_schema: JsonObject,
|
|
outcomes: list[str] | tuple[str, ...],
|
|
) -> WrapperAuthoringHints:
|
|
"""Derive conservative wrapper scaffolding for one workflow capability."""
|
|
input_properties = _object_properties(input_schema)
|
|
output_properties = _object_properties(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)}
|
|
state_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
name: schema for name, schema in sorted(output_properties.items())
|
|
},
|
|
}
|
|
wrapper_output_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
name: schema for name, schema in sorted(output_properties.items())
|
|
},
|
|
}
|
|
missing_decisions = _missing_decisions_for_output(output_schema)
|
|
outcome_candidates = _boolean_outcome_candidates(output_properties)
|
|
if outcome_candidates:
|
|
missing_decisions.append(
|
|
MissingDecision(
|
|
kind=MissingDecisionKind.CONFIRM_BOOLEAN_OUTCOMES,
|
|
message="Confirm whether boolean output fields should control wrapper routing.",
|
|
)
|
|
)
|
|
confidence = _confidence_for_hint(
|
|
input_schema=input_schema,
|
|
output_schema=output_schema,
|
|
missing_decisions=missing_decisions,
|
|
outcome_candidates=outcome_candidates,
|
|
)
|
|
return WrapperAuthoringHints(
|
|
capability_name=capability_name,
|
|
confidence=confidence,
|
|
declared_outcomes=list(outcomes),
|
|
suggested_wrapper_outcomes=list(outcomes),
|
|
outcome_policy=WrapperOutcomePolicy.PRESERVE_DECLARED,
|
|
input_schema=input_schema,
|
|
state_schema=state_schema,
|
|
output_schema=wrapper_output_schema,
|
|
input_map=input_map,
|
|
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.",
|
|
],
|
|
)
|
|
|
|
|
|
def _object_properties(schema: JsonObject) -> dict[str, JsonObject]:
|
|
properties = schema.get("properties")
|
|
if not isinstance(properties, dict):
|
|
return {}
|
|
return {
|
|
str(name): value
|
|
for name, value in properties.items()
|
|
if isinstance(value, dict)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Implement confidence and missing decision helpers**
|
|
|
|
Add below `_object_properties` in `src/wf_mcp/workflow_surface/wrapper_hints.py`:
|
|
|
|
```python
|
|
def _missing_decisions_for_output(output_schema: JsonObject) -> list[MissingDecision]:
|
|
properties = _object_properties(output_schema)
|
|
if not properties:
|
|
return [
|
|
MissingDecision(
|
|
kind=MissingDecisionKind.CHOOSE_OUTPUT_FIELDS,
|
|
message="Capability output schema has no top-level object properties to map.",
|
|
)
|
|
]
|
|
decisions: list[MissingDecision] = []
|
|
for name, schema in sorted(properties.items()):
|
|
schema_type = schema.get("type")
|
|
if schema_type == "object" or schema_type == "array":
|
|
decisions.append(
|
|
MissingDecision(
|
|
kind=MissingDecisionKind.REVIEW_NESTED_OUTPUT,
|
|
message=f"Review output.{name}; nested or collection outputs may need explicit mapping.",
|
|
)
|
|
)
|
|
return decisions
|
|
|
|
|
|
def _boolean_outcome_candidates(
|
|
output_properties: dict[str, JsonObject],
|
|
) -> list[OutcomeCandidate]:
|
|
candidates: list[OutcomeCandidate] = []
|
|
for name, schema in sorted(output_properties.items()):
|
|
if schema.get("type") != "boolean":
|
|
continue
|
|
if name.casefold() not in CONTROL_BOOLEAN_NAMES:
|
|
continue
|
|
candidates.append(
|
|
OutcomeCandidate(
|
|
kind=OutcomeCandidateKind.BOOLEAN_CONTROL_FIELD,
|
|
source=f"output.{name}",
|
|
candidate_outcomes=_candidate_outcomes_for_boolean_name(name),
|
|
confidence=WrapperHintConfidence.MEDIUM,
|
|
reason="top-level boolean field with control-like name",
|
|
automatic=False,
|
|
)
|
|
)
|
|
return candidates
|
|
|
|
|
|
def _candidate_outcomes_for_boolean_name(name: str) -> list[str]:
|
|
normalized = name.casefold()
|
|
if normalized in {"success", "ok", "done", "complete"}:
|
|
return ["success", "failure"]
|
|
if normalized in {"failed", "error", "is_error"}:
|
|
return ["error", "ok"]
|
|
if normalized in {"approved", "rejected"}:
|
|
return ["approved", "rejected"]
|
|
if normalized in {"needs_input", "requires_approval"}:
|
|
return [normalized, "done"]
|
|
if normalized == "has_more":
|
|
return ["has_more", "done"]
|
|
return ["true", "false"]
|
|
|
|
|
|
def _confidence_for_hint(
|
|
*,
|
|
input_schema: JsonObject,
|
|
output_schema: JsonObject,
|
|
missing_decisions: list[MissingDecision],
|
|
outcome_candidates: list[OutcomeCandidate],
|
|
) -> WrapperHintConfidence:
|
|
if not _object_properties(input_schema) or not _object_properties(output_schema):
|
|
return WrapperHintConfidence.LOW
|
|
if any(
|
|
decision.kind == MissingDecisionKind.REVIEW_NESTED_OUTPUT
|
|
for decision in missing_decisions
|
|
):
|
|
return WrapperHintConfidence.LOW
|
|
if missing_decisions or outcome_candidates:
|
|
return WrapperHintConfidence.MEDIUM
|
|
return WrapperHintConfidence.HIGH
|
|
```
|
|
|
|
- [ ] **Step 5: Run wrapper hint tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_wrapper_hints.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
## Task 3: Add Boolean Outcome Candidate Tests
|
|
|
|
**Files:**
|
|
|
|
- Modify: `tests/wf_mcp/test_workflow_wrapper_hints.py`
|
|
- Modify: `src/wf_mcp/workflow_surface/wrapper_hints.py` only if tests reveal gaps.
|
|
|
|
- [ ] **Step 1: Add candidate and non-candidate tests**
|
|
|
|
Append:
|
|
|
|
```python
|
|
def test_wrapper_hints_offer_boolean_outcome_candidates_without_auto_mapping() -> None:
|
|
hints = wrapper_hints_for_capability(
|
|
capability_name="demo.personal.submit",
|
|
input_schema={"type": "object", "properties": {"text": {"type": "string"}}},
|
|
output_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"success": {"type": "boolean"},
|
|
"message": {"type": "string"},
|
|
},
|
|
},
|
|
outcomes=["ok"],
|
|
)
|
|
|
|
dumped = hints.model_dump(mode="json")
|
|
candidate = dumped["outcome_candidates"][0]
|
|
|
|
assert dumped["confidence"] == "medium"
|
|
assert candidate["kind"] == "boolean_control_field"
|
|
assert candidate["source"] == "output.success"
|
|
assert candidate["candidate_outcomes"] == ["success", "failure"]
|
|
assert candidate["automatic"] is False
|
|
assert dumped["outcome_policy"] == "preserve_declared"
|
|
assert dumped["suggested_wrapper_outcomes"] == ["ok"]
|
|
assert dumped["missing_decisions"][0]["kind"] == "confirm_boolean_outcomes"
|
|
|
|
|
|
def test_wrapper_hints_do_not_treat_arbitrary_booleans_as_outcomes() -> None:
|
|
hints = wrapper_hints_for_capability(
|
|
capability_name="demo.personal.profile",
|
|
input_schema={"type": "object", "properties": {"user_id": {"type": "string"}}},
|
|
output_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"is_admin": {"type": "boolean"},
|
|
"name": {"type": "string"},
|
|
},
|
|
},
|
|
outcomes=["ok"],
|
|
)
|
|
|
|
dumped = hints.model_dump(mode="json")
|
|
|
|
assert dumped["confidence"] == "high"
|
|
assert dumped["outcome_candidates"] == []
|
|
assert dumped["missing_decisions"] == []
|
|
```
|
|
|
|
- [ ] **Step 2: Run tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_wrapper_hints.py -q
|
|
```
|
|
|
|
Expected: pass. If arbitrary boolean fields produce candidates, fix `CONTROL_BOOLEAN_NAMES` filtering rather than deleting the test.
|
|
|
|
## Task 4: Add Complex Output Missing Decision Tests
|
|
|
|
**Files:**
|
|
|
|
- Modify: `tests/wf_mcp/test_workflow_wrapper_hints.py`
|
|
- Modify: `src/wf_mcp/workflow_surface/wrapper_hints.py` only if tests reveal gaps.
|
|
|
|
- [ ] **Step 1: Add nested-output and empty-output tests**
|
|
|
|
Append:
|
|
|
|
```python
|
|
def test_wrapper_hints_mark_nested_outputs_as_low_confidence() -> None:
|
|
hints = wrapper_hints_for_capability(
|
|
capability_name="demo.personal.search",
|
|
input_schema={"type": "object", "properties": {"query": {"type": "string"}}},
|
|
output_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"results": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {"title": {"type": "string"}},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
outcomes=["ok"],
|
|
)
|
|
|
|
dumped = hints.model_dump(mode="json")
|
|
|
|
assert dumped["confidence"] == "low"
|
|
assert dumped["missing_decisions"][0]["kind"] == "review_nested_output"
|
|
assert dumped["output_map"] == {"results": "state.results"}
|
|
|
|
|
|
def test_wrapper_hints_mark_empty_output_schema_as_low_confidence() -> None:
|
|
hints = wrapper_hints_for_capability(
|
|
capability_name="demo.personal.no_output",
|
|
input_schema={"type": "object", "properties": {"text": {"type": "string"}}},
|
|
output_schema={"type": "object", "properties": {}},
|
|
outcomes=["ok"],
|
|
)
|
|
|
|
dumped = hints.model_dump(mode="json")
|
|
|
|
assert dumped["confidence"] == "low"
|
|
assert dumped["input_map"] == {"input.text": "text"}
|
|
assert dumped["output_map"] == {}
|
|
assert dumped["missing_decisions"][0]["kind"] == "choose_output_fields"
|
|
```
|
|
|
|
- [ ] **Step 2: Run tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_wrapper_hints.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
## Task 5: Wire Hints Into `inspect_capability`
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
|
|
- Test: `tests/wf_mcp/test_workflow_surface.py`
|
|
|
|
- [ ] **Step 1: Add failing live capability integration test**
|
|
|
|
In `tests/wf_mcp/test_workflow_surface.py`, after `test_workflow_surface_inspects_one_capability`, add:
|
|
|
|
```python
|
|
def test_workflow_surface_inspect_capability_includes_wrapper_hints() -> None:
|
|
service = WfMcpService(
|
|
store=FileStore(local_temp_root() / "surface_wrapper_hints_mcp"),
|
|
artifact_store=FileWorkflowArtifactStore(
|
|
local_temp_root() / "surface_wrapper_hints_artifacts"
|
|
),
|
|
)
|
|
service.register_connection(
|
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
)
|
|
service.register_specs("demo.personal", echo_tool)
|
|
handlers = WorkflowSurfaceHandlers(service)
|
|
|
|
payload = asyncio.run(
|
|
handlers.inspect_capability(qualified_name="demo.personal.echo_tool")
|
|
)
|
|
|
|
hints = payload["wrapper_hints"]
|
|
assert hints["capability_name"] == "demo.personal.echo_tool"
|
|
assert hints["declared_outcomes"] == ["ok"]
|
|
assert hints["input_map"] == {"input.text": "text"}
|
|
assert hints["output_map"] == {"echoed": "state.echoed"}
|
|
assert hints["outcome_policy"] == "preserve_declared"
|
|
```
|
|
|
|
- [ ] **Step 2: Run integration test to verify it fails**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py::test_workflow_surface_inspect_capability_includes_wrapper_hints -q
|
|
```
|
|
|
|
Expected: failure because `wrapper_hints` is absent.
|
|
|
|
- [ ] **Step 3: Attach hints for live NodeSpecs**
|
|
|
|
In `src/wf_mcp/workflow_surface/handlers.py`, import:
|
|
|
|
```python
|
|
from .wrapper_hints import wrapper_hints_for_capability
|
|
```
|
|
|
|
Then in `inspect_capability`, replace the live-detail return with:
|
|
|
|
```python
|
|
detail_payload = detail.model_dump(mode="json")
|
|
detail_payload["wrapper_hints"] = wrapper_hints_for_capability(
|
|
capability_name=detail.name,
|
|
input_schema=detail.input_schema,
|
|
output_schema=detail.output_schema,
|
|
outcomes=detail.outcomes,
|
|
).model_dump(mode="json")
|
|
return detail_payload
|
|
```
|
|
|
|
- [ ] **Step 4: Run integration test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py::test_workflow_surface_inspect_capability_includes_wrapper_hints -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
## Task 6: Add Hints For Saved Wrapper Artifact Inspection
|
|
|
|
**Files:**
|
|
|
|
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
|
|
- Test: `tests/wf_mcp/test_workflow_surface.py`
|
|
|
|
- [ ] **Step 1: Add failing saved-wrapper hint assertion**
|
|
|
|
In existing `test_workflow_surface_inspects_saved_wrapper_capability`, add:
|
|
|
|
```python
|
|
hints = payload["wrapper_hints"]
|
|
assert hints["capability_name"] == "workflow.echo_wrapper.v1"
|
|
assert hints["declared_outcomes"] == ["completed"]
|
|
assert hints["suggested_wrapper_outcomes"] == ["completed"]
|
|
assert hints["input_map"] == {"input.text": "text"}
|
|
assert hints["output_map"] == {"echoed": "state.echoed"}
|
|
```
|
|
|
|
- [ ] **Step 2: Run saved-wrapper test to verify it fails**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py::test_workflow_surface_inspects_saved_wrapper_capability -q
|
|
```
|
|
|
|
Expected: failure because wrapper detail lacks `wrapper_hints`.
|
|
|
|
- [ ] **Step 3: Attach hints for wrapper artifacts**
|
|
|
|
In `_wrapper_capability_detail`, add `wrapper_hints` to the returned dict:
|
|
|
|
```python
|
|
"wrapper_hints": wrapper_hints_for_capability(
|
|
capability_name=_artifact_capability_id(artifact),
|
|
input_schema=artifact.input_schema,
|
|
output_schema=artifact.output_schema,
|
|
outcomes=list(artifact.outcomes),
|
|
).model_dump(mode="json"),
|
|
```
|
|
|
|
- [ ] **Step 4: Run saved-wrapper test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_surface.py::test_workflow_surface_inspects_saved_wrapper_capability -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
## Task 7: Document Hint Semantics
|
|
|
|
**Files:**
|
|
|
|
- Modify: `docs/workflow_capabilities.md`
|
|
|
|
- [ ] **Step 1: Add documentation section**
|
|
|
|
Add a section titled `## Wrapper Authoring Hints`:
|
|
|
|
```markdown
|
|
## Wrapper Authoring Hints
|
|
|
|
`wf.workflow.inspect_capability` returns `wrapper_hints` for planner-visible
|
|
capabilities. These hints are scaffolding for draft creation, not semantic
|
|
guarantees.
|
|
|
|
Declared capability outcomes are authoritative and are preserved by default.
|
|
Boolean output fields may appear as `outcome_candidates` when they have
|
|
control-like names such as `success`, `error`, `approved`, or `has_more`, but
|
|
they are never wired automatically. A wrapper author must explicitly confirm
|
|
whether those fields should become routing conditions.
|
|
|
|
`confidence` is intentionally coarse:
|
|
|
|
- `high`: simple object input/output schemas and no missing decisions.
|
|
- `medium`: usable scaffold with candidate decisions, such as boolean outcome
|
|
candidates.
|
|
- `low`: missing or nested output choices require explicit authoring.
|
|
|
|
`missing_decisions` is a typed list of decisions the author should resolve
|
|
before saving a wrapper. MCP clients should show these prominently rather than
|
|
treating the scaffold as complete.
|
|
```
|
|
|
|
- [ ] **Step 2: Run docs-adjacent tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_wrapper_hints.py tests/wf_mcp/test_workflow_surface.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
## Task 8: Final Verification
|
|
|
|
**Files:**
|
|
|
|
- All touched files.
|
|
|
|
- [ ] **Step 1: Run focused tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest tests/wf_mcp/test_workflow_wrapper_hints.py tests/wf_mcp/test_workflow_surface.py tests/wf_mcp/test_workflow_surface_refs.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
- [ ] **Step 2: Run full tests if the workspace is not mid-edit**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run --with pytest pytest -q
|
|
```
|
|
|
|
Expected: pass, allowing intentional environment-dependent skips. If the user is actively editing `tests/rewrite`, run focused tests only and state that full-suite verification was deferred.
|
|
|
|
- [ ] **Step 3: Run static checks**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uvx ruff check src/wf_mcp/workflow_surface tests/wf_mcp/test_workflow_wrapper_hints.py tests/wf_mcp/test_workflow_surface.py
|
|
uv run basedpyright --level error src/wf_mcp/workflow_surface tests/wf_mcp/test_workflow_wrapper_hints.py tests/wf_mcp/test_workflow_surface.py
|
|
uvx ruff format --check src/wf_mcp/workflow_surface tests/wf_mcp/test_workflow_wrapper_hints.py tests/wf_mcp/test_workflow_surface.py
|
|
```
|
|
|
|
Expected: ruff passes, basedpyright has 0 errors, and formatting is clean.
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage: The plan covers enum-backed type fields, conservative outcome suggestions, boolean candidates, confidence/missing-decision UX, inspect-capability integration, and docs.
|
|
- Placeholder scan: No `TBD`, `TODO`, or unspecified implementation steps remain.
|
|
- Type consistency: `WrapperHintConfidence`, `WrapperOutcomePolicy`, `OutcomeCandidateKind`, and `MissingDecisionKind` are defined before use and serialize through Pydantic models.
|
|
- Scope check: This plan does not create or save wrappers automatically; it only adds hint payloads for authoring.
|