next action hints
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
# Wrapper Draft Next Actions 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 advisory `next_actions` to `wf.workflow.create_draft_workspace_from_capability` so LLM clients can continue safely even when they cannot easily read the docs.
|
||||
|
||||
**Architecture:** Keep `wrapper_hints` as the source of truth for scaffold confidence, missing decisions, and mapping warnings. Add a small typed result object that converts those hints into concrete next-tool guidance. `can_save_now` is advisory only; do not block saving or enforce policy in this slice.
|
||||
|
||||
**Tech Stack:** Python 3.14, Pydantic v2, FastMCP tool schemas, pytest, ruff, basedpyright.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
Add `next_actions` to `CreateDraftWorkspaceFromCapabilityResult` and handler payloads.
|
||||
|
||||
Do:
|
||||
|
||||
- Return machine-readable guidance from `create_draft_workspace_from_capability`.
|
||||
- Make `can_save_now` advisory only.
|
||||
- Recommend `wf.workflow.patch_draft_workspace` when `wrapper_hints.missing_decisions` is non-empty or confidence is low.
|
||||
- Recommend `wf.workflow.validate_draft_workspace` when the scaffold looks safe enough to validate.
|
||||
- Include patch examples for common missing decisions.
|
||||
- Document the field and add schema tests.
|
||||
|
||||
Do not:
|
||||
|
||||
- Block `create_artifact_from_workspace`.
|
||||
- Add a new tool.
|
||||
- Infer raw MCP `content[0].text` automatically.
|
||||
- Treat boolean output candidates as real routing semantics.
|
||||
- Replace `wrapper_hints`; `next_actions` should summarize and guide, not duplicate every hint.
|
||||
|
||||
## Files
|
||||
|
||||
- Modify: `src/wf_mcp/workflow_surface/models.py`
|
||||
- Add typed `WrapperDraftNextActions` and `WrapperDraftPatchExample` Pydantic models.
|
||||
- Add `next_actions` field to `CreateDraftWorkspaceFromCapabilityResult`.
|
||||
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
|
||||
- Add helper that derives `next_actions` from `wrapper_hints` and workspace id/revision.
|
||||
- Include `next_actions` in `create_draft_workspace_from_capability` result.
|
||||
- Test: `tests/wf_mcp/workflow_surface/test_drafts.py`
|
||||
- Assert high-confidence draft returns validate/save guidance.
|
||||
- Assert low-confidence content-block draft returns patch guidance and advisory `can_save_now=false`.
|
||||
- Test: `tests/wf_mcp/server/test_config.py`
|
||||
- Assert output schema exposes `next_actions` with field descriptions.
|
||||
- Modify: `docs/workflow_capabilities.md`
|
||||
- Explain `next_actions` as advisory continuation hints.
|
||||
- Modify: `docs/wf_mcp_end_to_end_runbook.md`
|
||||
- Mention that clients can follow `next_actions` after draft creation.
|
||||
|
||||
## Data Shape
|
||||
|
||||
Add this output shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"next_actions": {
|
||||
"can_save_now": true,
|
||||
"recommended_next_tool": "wf.workflow.validate_draft_workspace",
|
||||
"reason": "Wrapper hints are high confidence and have no missing decisions.",
|
||||
"patch_examples": [],
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Low-confidence example:
|
||||
|
||||
```json
|
||||
{
|
||||
"next_actions": {
|
||||
"can_save_now": false,
|
||||
"recommended_next_tool": "wf.workflow.patch_draft_workspace",
|
||||
"reason": "Review missing wrapper decisions before saving.",
|
||||
"patch_examples": [
|
||||
{
|
||||
"description": "Replace the output bindings after choosing workflow state fields.",
|
||||
"tool": "wf.workflow.patch_draft_workspace",
|
||||
"request": {
|
||||
"workspace_id": "echo_wrapper",
|
||||
"revision": 1,
|
||||
"patch": [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/draft/steps/call/output",
|
||||
"value": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [
|
||||
"Raw MCP content blocks are not workflow-shaped. Use an explicit wrapper or extraction node."
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Task 1: Add Result Models And Schema Test
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/wf_mcp/workflow_surface/models.py`
|
||||
- Modify: `tests/wf_mcp/server/test_config.py`
|
||||
|
||||
- [ ] **Step 1: Add failing schema assertions**
|
||||
|
||||
In `tests/wf_mcp/server/test_config.py`, inside the test that already inspects `create_draft_workspace_from_capability` output schema, add:
|
||||
|
||||
```python
|
||||
assert "next_actions" in from_capability_output["properties"]
|
||||
next_actions_schema = from_capability_output["properties"]["next_actions"]
|
||||
assert "recommended_next_tool" in next_actions_schema["properties"]
|
||||
assert "patch_examples" in next_actions_schema["properties"]
|
||||
assert "advisory" in next_actions_schema["properties"]["can_save_now"]["description"]
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_mcp/server/test_config.py -q
|
||||
```
|
||||
|
||||
Expected: fail because `next_actions` is not in the output schema.
|
||||
|
||||
- [ ] **Step 2: Add Pydantic result models**
|
||||
|
||||
In `src/wf_mcp/workflow_surface/models.py`, add near `CreateDraftWorkspaceFromCapabilityResult`:
|
||||
|
||||
```python
|
||||
class WrapperDraftPatchExample(BaseModel):
|
||||
"""Concrete patch-workspace example for a likely next authoring edit."""
|
||||
|
||||
description: str = Field(description="Human-readable reason for this patch.")
|
||||
tool: str = Field(description="MCP tool to call for this example.")
|
||||
request: dict[str, Any] = Field(
|
||||
description="JSON request payload to pass to the tool."
|
||||
)
|
||||
|
||||
|
||||
class WrapperDraftNextActions(BaseModel):
|
||||
"""Advisory continuation hints after bootstrapping a wrapper draft."""
|
||||
|
||||
can_save_now: bool = Field(
|
||||
description=(
|
||||
"Advisory only. False means the scaffold likely needs review before "
|
||||
"saving, but the server does not enforce this."
|
||||
)
|
||||
)
|
||||
recommended_next_tool: str = Field(
|
||||
description=(
|
||||
"Suggested next MCP tool, usually wf.workflow.validate_draft_workspace "
|
||||
"or wf.workflow.patch_draft_workspace."
|
||||
)
|
||||
)
|
||||
reason: str = Field(description="Short explanation for the recommendation.")
|
||||
patch_examples: list[WrapperDraftPatchExample] = Field(
|
||||
default_factory=list,
|
||||
description="Concrete JSON Patch examples for common missing decisions.",
|
||||
)
|
||||
warnings: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Non-blocking warnings copied from low-confidence wrapper hints.",
|
||||
)
|
||||
```
|
||||
|
||||
Then update:
|
||||
|
||||
```python
|
||||
class CreateDraftWorkspaceFromCapabilityResult(DraftWorkspaceResult):
|
||||
"""Draft workspace result plus wrapper hints and advisory next actions."""
|
||||
|
||||
wrapper_hints: dict[str, Any] = Field(...)
|
||||
next_actions: WrapperDraftNextActions = Field(
|
||||
description=(
|
||||
"Advisory next step guidance derived from wrapper_hints. "
|
||||
"The server does not enforce can_save_now."
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run schema test**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_mcp/server/test_config.py -q
|
||||
```
|
||||
|
||||
Expected: schema assertion passes, but runtime tests may still fail later until handler returns `next_actions`.
|
||||
|
||||
## Task 2: Derive Next Actions In The Handler
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/wf_mcp/workflow_surface/handlers.py`
|
||||
- Modify: `tests/wf_mcp/workflow_surface/test_drafts.py`
|
||||
|
||||
- [ ] **Step 1: Add high-confidence behavior test**
|
||||
|
||||
In `tests/wf_mcp/workflow_surface/test_drafts.py`, extend `test_workflow_surface_creates_draft_workspace_from_capability_hints`:
|
||||
|
||||
```python
|
||||
next_actions = result["next_actions"]
|
||||
assert next_actions["can_save_now"] is True
|
||||
assert next_actions["recommended_next_tool"] == "wf.workflow.validate_draft_workspace"
|
||||
assert "high confidence" in next_actions["reason"]
|
||||
assert next_actions["patch_examples"] == []
|
||||
assert next_actions["warnings"] == []
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_mcp/workflow_surface/test_drafts.py::test_workflow_surface_creates_draft_workspace_from_capability_hints -q
|
||||
```
|
||||
|
||||
Expected: fail because handler does not return `next_actions`.
|
||||
|
||||
- [ ] **Step 2: Add low-confidence behavior test**
|
||||
|
||||
Find or create a content-block capability test near existing wrapper hint tests. Use an output schema shaped like:
|
||||
|
||||
```python
|
||||
content_output_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"type": "string"},
|
||||
"text": {"type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Register a NodeSpec/capability with that output schema, call `create_draft_workspace_from_capability`, then assert:
|
||||
|
||||
```python
|
||||
next_actions = result["next_actions"]
|
||||
assert next_actions["can_save_now"] is False
|
||||
assert next_actions["recommended_next_tool"] == "wf.workflow.patch_draft_workspace"
|
||||
assert "missing wrapper decisions" in next_actions["reason"]
|
||||
assert next_actions["patch_examples"][0]["tool"] == "wf.workflow.patch_draft_workspace"
|
||||
assert next_actions["patch_examples"][0]["request"]["workspace_id"] == "content_wrapper"
|
||||
assert next_actions["patch_examples"][0]["request"]["revision"] == result["revision"]
|
||||
assert next_actions["warnings"]
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_mcp/workflow_surface/test_drafts.py -q
|
||||
```
|
||||
|
||||
Expected: fail until implementation exists.
|
||||
|
||||
- [ ] **Step 3: Implement helper in handler**
|
||||
|
||||
In `src/wf_mcp/workflow_surface/handlers.py`, add a helper near other draft helpers:
|
||||
|
||||
```python
|
||||
def _wrapper_draft_next_actions(
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
hints: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Convert wrapper_hints into advisory next-tool guidance for MCP clients."""
|
||||
confidence = str(hints.get("confidence", "low"))
|
||||
missing_decisions = hints.get("missing_decisions")
|
||||
notes = [str(note) for note in hints.get("notes", []) if isinstance(note, str)]
|
||||
has_missing = isinstance(missing_decisions, list) and len(missing_decisions) > 0
|
||||
can_save_now = confidence == "high" and not has_missing
|
||||
if can_save_now:
|
||||
return {
|
||||
"can_save_now": True,
|
||||
"recommended_next_tool": "wf.workflow.validate_draft_workspace",
|
||||
"reason": "Wrapper hints are high confidence and have no missing decisions.",
|
||||
"patch_examples": [],
|
||||
"warnings": [],
|
||||
}
|
||||
|
||||
return {
|
||||
"can_save_now": False,
|
||||
"recommended_next_tool": "wf.workflow.patch_draft_workspace",
|
||||
"reason": "Review missing wrapper decisions before saving.",
|
||||
"patch_examples": _wrapper_draft_patch_examples(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
hints=hints,
|
||||
),
|
||||
"warnings": notes,
|
||||
}
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
def _wrapper_draft_patch_examples(
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
hints: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return conservative JSON Patch examples without guessing semantics."""
|
||||
examples: list[dict[str, Any]] = []
|
||||
missing_decisions = hints.get("missing_decisions")
|
||||
if not isinstance(missing_decisions, list):
|
||||
return examples
|
||||
decision_kinds = {
|
||||
str(decision.get("kind"))
|
||||
for decision in missing_decisions
|
||||
if isinstance(decision, dict)
|
||||
}
|
||||
if {
|
||||
"choose_output_fields",
|
||||
"review_nested_output",
|
||||
} & decision_kinds:
|
||||
examples.append(
|
||||
{
|
||||
"description": (
|
||||
"Replace output bindings after choosing which capability "
|
||||
"outputs should be written to workflow state."
|
||||
),
|
||||
"tool": "wf.workflow.patch_draft_workspace",
|
||||
"request": {
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"patch": [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/draft/steps/call/output",
|
||||
"value": [],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
if "confirm_boolean_outcomes" in decision_kinds:
|
||||
examples.append(
|
||||
{
|
||||
"description": (
|
||||
"Review boolean output candidates before adding routing; "
|
||||
"do not route on boolean fields automatically."
|
||||
),
|
||||
"tool": "wf.workflow.patch_draft_workspace",
|
||||
"request": {
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"patch": [],
|
||||
},
|
||||
}
|
||||
)
|
||||
return examples
|
||||
```
|
||||
|
||||
Then change the return in `create_draft_workspace_from_capability`:
|
||||
|
||||
```python
|
||||
return {
|
||||
**result,
|
||||
"wrapper_hints": hints,
|
||||
"next_actions": _wrapper_draft_next_actions(
|
||||
workspace_id=workspace_id,
|
||||
revision=int(result["revision"]),
|
||||
hints=hints,
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run behavior tests**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
## Task 3: Document Next Actions
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/workflow_capabilities.md`
|
||||
- Modify: `docs/wf_mcp_end_to_end_runbook.md`
|
||||
|
||||
- [ ] **Step 1: Update workflow capability docs**
|
||||
|
||||
In `docs/workflow_capabilities.md`, near the `wrapper_hints` section, add:
|
||||
|
||||
```markdown
|
||||
`create_draft_workspace_from_capability` also returns `next_actions`.
|
||||
This is advisory guidance for clients that cannot easily read the full docs.
|
||||
It summarizes whether the scaffold is safe-looking enough to validate, which
|
||||
tool to call next, and concrete patch examples for common missing decisions.
|
||||
|
||||
`next_actions.can_save_now` is not enforced. A caller can still save a low
|
||||
confidence draft, but the field exists to make that risk explicit.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update runbook**
|
||||
|
||||
In `docs/wf_mcp_end_to_end_runbook.md`, near the draft-from-capability example, add:
|
||||
|
||||
```markdown
|
||||
After `create_draft_workspace_from_capability`, inspect `next_actions`.
|
||||
If `recommended_next_tool` is `wf.workflow.patch_draft_workspace`, apply or
|
||||
adapt the returned `patch_examples` before saving. If it recommends
|
||||
`wf.workflow.validate_draft_workspace`, validate the draft before creating an
|
||||
artifact.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Grep docs**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
rg -n "next_actions|can_save_now|patch_examples" docs/workflow_capabilities.md docs/wf_mcp_end_to_end_runbook.md
|
||||
```
|
||||
|
||||
Expected: all three terms appear.
|
||||
|
||||
## Task 4: Final Verification
|
||||
|
||||
**Files:**
|
||||
|
||||
- All touched files.
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run pytest tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py tests/wf_mcp/test_workflow_wrapper_hints.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
- [ ] **Step 2: Run touched-file lint and format check**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run ruff check src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/handlers.py tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py
|
||||
uv run ruff format --check src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/handlers.py tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
- [ ] **Step 3: Run touched-file type check**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
uv run basedpyright --level error src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/handlers.py tests/wf_mcp/workflow_surface/test_drafts.py tests/wf_mcp/server/test_config.py
|
||||
```
|
||||
|
||||
Expected: `0 errors`.
|
||||
|
||||
- [ ] **Step 4: Optional full suite**
|
||||
|
||||
Run when time allows:
|
||||
|
||||
```powershell
|
||||
uv run pytest -q
|
||||
```
|
||||
|
||||
Expected current baseline: full suite passes with the existing skip/xfail count.
|
||||
|
||||
## Notes For Opencode
|
||||
|
||||
- `can_save_now` is advisory. Do not enforce it.
|
||||
- Do not add a new save gate.
|
||||
- Do not generate semantic routes from boolean fields.
|
||||
- Keep patch examples conservative; empty patch examples are acceptable when the missing decision cannot be safely represented.
|
||||
- Avoid whole-dict assertions. Assert individual fields.
|
||||
- Existing docs may be long; keep additions short.
|
||||
@@ -649,6 +649,12 @@ arguments:
|
||||
This creates a mutable, revisioned workspace using the inspected
|
||||
`wrapper_hints`.
|
||||
|
||||
After `create_draft_workspace_from_capability`, inspect `next_actions`.
|
||||
If `recommended_next_tool` is `wf.workflow.patch_draft_workspace`, apply or
|
||||
adapt the returned `patch_examples` before saving. If it recommends
|
||||
`wf.workflow.validate_draft_workspace`, validate the draft before creating an
|
||||
artifact.
|
||||
|
||||
### 3. Patch Or Validate The Workspace
|
||||
|
||||
If the hints are good enough, validate:
|
||||
|
||||
@@ -346,6 +346,14 @@ whether those fields should become routing conditions.
|
||||
before saving a wrapper. MCP clients should show these prominently rather than
|
||||
treating the scaffold as complete.
|
||||
|
||||
`create_draft_workspace_from_capability` also returns `next_actions`.
|
||||
This is advisory guidance for clients that cannot easily read the full docs.
|
||||
It summarizes whether the scaffold is safe-looking enough to validate, which
|
||||
tool to call next, and concrete patch examples for common missing decisions.
|
||||
|
||||
`next_actions.can_save_now` is not enforced. A caller can still save a low
|
||||
confidence draft, but the field exists to make that risk explicit.
|
||||
|
||||
## Relationship To Capability Sources
|
||||
|
||||
Sources own capability kinds:
|
||||
|
||||
@@ -833,7 +833,15 @@ class WorkflowSurfaceHandlers:
|
||||
error_message_source=error_message_source,
|
||||
title=title,
|
||||
)
|
||||
return {**result, "wrapper_hints": hints}
|
||||
return {
|
||||
**result,
|
||||
"wrapper_hints": hints,
|
||||
"next_actions": _wrapper_draft_next_actions(
|
||||
workspace_id=workspace_id,
|
||||
revision=int(result["revision"]),
|
||||
hints=hints,
|
||||
),
|
||||
}
|
||||
|
||||
async def create_artifact_from_workspace(
|
||||
self,
|
||||
@@ -1478,6 +1486,98 @@ def _draft_name_from_capability(capability_name: str) -> str:
|
||||
return capability_name.replace(".", "_").replace("-", "_")
|
||||
|
||||
|
||||
def _wrapper_draft_next_actions(
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
hints: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Convert wrapper_hints into advisory next-tool guidance for MCP clients."""
|
||||
confidence = str(hints.get("confidence", "low"))
|
||||
missing_decisions = hints.get("missing_decisions")
|
||||
notes = [str(note) for note in hints.get("notes", []) if isinstance(note, str)]
|
||||
has_missing = isinstance(missing_decisions, list) and len(missing_decisions) > 0
|
||||
can_save_now = confidence == "high" and not has_missing
|
||||
if can_save_now:
|
||||
return {
|
||||
"can_save_now": True,
|
||||
"recommended_next_tool": "wf.workflow.validate_draft_workspace",
|
||||
"reason": "Wrapper hints are high confidence and have no missing decisions.",
|
||||
"patch_examples": [],
|
||||
"warnings": [],
|
||||
}
|
||||
|
||||
return {
|
||||
"can_save_now": False,
|
||||
"recommended_next_tool": "wf.workflow.patch_draft_workspace",
|
||||
"reason": "Review missing wrapper decisions before saving.",
|
||||
"patch_examples": _wrapper_draft_patch_examples(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
hints=hints,
|
||||
),
|
||||
"warnings": notes,
|
||||
}
|
||||
|
||||
|
||||
def _wrapper_draft_patch_examples(
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
hints: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return conservative JSON Patch examples without guessing semantics."""
|
||||
examples: list[dict[str, Any]] = []
|
||||
missing_decisions = hints.get("missing_decisions")
|
||||
if not isinstance(missing_decisions, list):
|
||||
return examples
|
||||
decision_kinds = {
|
||||
str(decision.get("kind"))
|
||||
for decision in missing_decisions
|
||||
if isinstance(decision, dict)
|
||||
}
|
||||
if {
|
||||
"choose_output_fields",
|
||||
"review_nested_output",
|
||||
} & decision_kinds:
|
||||
examples.append(
|
||||
{
|
||||
"description": (
|
||||
"Replace output bindings after choosing which capability "
|
||||
"outputs should be written to workflow state."
|
||||
),
|
||||
"tool": "wf.workflow.patch_draft_workspace",
|
||||
"request": {
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"patch": [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/draft/steps/call/output",
|
||||
"value": [],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
if "confirm_boolean_outcomes" in decision_kinds:
|
||||
examples.append(
|
||||
{
|
||||
"description": (
|
||||
"Review boolean output candidates before adding routing; "
|
||||
"do not route on boolean fields automatically."
|
||||
),
|
||||
"tool": "wf.workflow.patch_draft_workspace",
|
||||
"request": {
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"patch": [],
|
||||
},
|
||||
}
|
||||
)
|
||||
return examples
|
||||
|
||||
|
||||
def _source_id_for_capability(
|
||||
sources: dict[str, CapabilitySource],
|
||||
qualified_name: str,
|
||||
|
||||
@@ -353,8 +353,44 @@ class CreateDraftWorkspaceFromCapabilityRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class WrapperDraftPatchExample(BaseModel):
|
||||
"""Concrete patch-workspace example for a likely next authoring edit."""
|
||||
|
||||
description: str = Field(description="Human-readable reason for this patch.")
|
||||
tool: str = Field(description="MCP tool to call for this example.")
|
||||
request: dict[str, Any] = Field(
|
||||
description="JSON request payload to pass to the tool."
|
||||
)
|
||||
|
||||
|
||||
class WrapperDraftNextActions(BaseModel):
|
||||
"""Advisory continuation hints after bootstrapping a wrapper draft."""
|
||||
|
||||
can_save_now: bool = Field(
|
||||
description=(
|
||||
"Advisory only. False means the scaffold likely needs review before "
|
||||
"saving, but the server does not enforce this."
|
||||
)
|
||||
)
|
||||
recommended_next_tool: str = Field(
|
||||
description=(
|
||||
"Suggested next MCP tool, usually wf.workflow.validate_draft_workspace "
|
||||
"or wf.workflow.patch_draft_workspace."
|
||||
)
|
||||
)
|
||||
reason: str = Field(description="Short explanation for the recommendation.")
|
||||
patch_examples: list[WrapperDraftPatchExample] = Field(
|
||||
default_factory=list,
|
||||
description="Concrete JSON Patch examples for common missing decisions.",
|
||||
)
|
||||
warnings: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Non-blocking warnings copied from low-confidence wrapper hints.",
|
||||
)
|
||||
|
||||
|
||||
class CreateDraftWorkspaceFromCapabilityResult(DraftWorkspaceResult):
|
||||
"""Draft workspace result plus the wrapper hints used to bootstrap it."""
|
||||
"""Draft workspace result plus wrapper hints and advisory next actions."""
|
||||
|
||||
wrapper_hints: dict[str, Any] = Field(
|
||||
description=(
|
||||
@@ -362,6 +398,12 @@ class CreateDraftWorkspaceFromCapabilityResult(DraftWorkspaceResult):
|
||||
"Use this to patch uncertain maps or schemas by revision."
|
||||
)
|
||||
)
|
||||
next_actions: WrapperDraftNextActions = Field(
|
||||
description=(
|
||||
"Advisory next step guidance derived from wrapper_hints. "
|
||||
"The server does not enforce can_save_now."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class CreateArtifactFromWorkspaceRequest(BaseModel):
|
||||
|
||||
@@ -112,6 +112,14 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
||||
].outputSchema
|
||||
assert from_capability_output is not None
|
||||
assert "wrapper_hints" in from_capability_output["properties"]
|
||||
assert "next_actions" in from_capability_output["properties"]
|
||||
next_actions_schema = from_capability_output["properties"]["next_actions"]
|
||||
assert "recommended_next_tool" in next_actions_schema["properties"]
|
||||
assert "patch_examples" in next_actions_schema["properties"]
|
||||
assert (
|
||||
"Advisory"
|
||||
in next_actions_schema["properties"]["can_save_now"]["description"]
|
||||
)
|
||||
wrapper_workspace_input = tools_by_name[
|
||||
"wf.workflow.create_wrapper_from_workspace"
|
||||
].inputSchema
|
||||
|
||||
@@ -13,6 +13,7 @@ from wf_core.paths import GraphSourcePath, LocalPath, StatePath
|
||||
|
||||
from ..test_support import echo_tool, local_temp_root
|
||||
from .conftest import (
|
||||
ContentOnlyOutputAdapter,
|
||||
echo_draft,
|
||||
handlers,
|
||||
mcp_echo_tool,
|
||||
@@ -539,6 +540,14 @@ def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> Non
|
||||
assert result["workspace_id"] == "echo_from_capability_canonical"
|
||||
assert result["wrapper_hints"]["input_map"] == {"input.text": "text"}
|
||||
assert result["wrapper_hints"]["output_map"] == {"echoed": "state.echoed"}
|
||||
next_actions = result["next_actions"]
|
||||
assert next_actions["can_save_now"] is True
|
||||
assert (
|
||||
next_actions["recommended_next_tool"] == "wf.workflow.validate_draft_workspace"
|
||||
)
|
||||
assert "high confidence" in next_actions["reason"]
|
||||
assert next_actions["patch_examples"] == []
|
||||
assert next_actions["warnings"] == []
|
||||
assert workspace.draft["steps"]["call"]["use"] == "demo.personal.echo_tool"
|
||||
assert workspace.draft["steps"]["call"]["input"] == [
|
||||
{
|
||||
@@ -668,3 +677,45 @@ def test_workflow_surface_creates_wrapper_from_workspace() -> None:
|
||||
assert result["saved"] is True
|
||||
assert artifact.kind == "wrapper"
|
||||
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
|
||||
|
||||
|
||||
def test_workflow_surface_low_confidence_draft_returns_patch_guidance() -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(
|
||||
local_temp_root() / "surface_workspace_low_confidence"
|
||||
)
|
||||
service = WfMcpService(
|
||||
store=FileStore(local_temp_root() / "surface_workspace_low_confidence_mcp"),
|
||||
artifact_store=artifact_store,
|
||||
)
|
||||
service.register_connection(
|
||||
ConnectionConfig(
|
||||
id="everything.default", server="everything", account="default"
|
||||
)
|
||||
)
|
||||
service.register_adapter("everything", ContentOnlyOutputAdapter())
|
||||
asyncio.run(service.refresh_connection_catalog("everything.default"))
|
||||
h = WorkflowSurfaceHandlers(service)
|
||||
|
||||
result = asyncio.run(
|
||||
h.create_draft_workspace_from_capability(
|
||||
workspace_id="content_wrapper",
|
||||
capability_name="everything.default.echo",
|
||||
name="content_wrapper",
|
||||
)
|
||||
)
|
||||
|
||||
next_actions = result["next_actions"]
|
||||
assert next_actions["can_save_now"] is False
|
||||
assert next_actions["recommended_next_tool"] == "wf.workflow.patch_draft_workspace"
|
||||
assert "missing wrapper decisions" in next_actions["reason"]
|
||||
assert (
|
||||
next_actions["patch_examples"][0]["tool"] == "wf.workflow.patch_draft_workspace"
|
||||
)
|
||||
assert (
|
||||
next_actions["patch_examples"][0]["request"]["workspace_id"]
|
||||
== "content_wrapper"
|
||||
)
|
||||
assert (
|
||||
next_actions["patch_examples"][0]["request"]["revision"] == result["revision"]
|
||||
)
|
||||
assert next_actions["warnings"]
|
||||
|
||||
Reference in New Issue
Block a user