docs update, allow some debugging

This commit is contained in:
lda
2026-05-21 16:11:11 +07:00 Verified
parent 046e2a6729
commit dcf7a6baf9
11 changed files with 429 additions and 5 deletions
+18
View File
@@ -14,6 +14,11 @@ on both the current docs and the implementation state.
- Make the LLM/client path progressive: inspect sources, create a draft,
patch, validate, compile, save, run.
- Prefer smaller discovery/inspection responses over one huge payload.
- Done: the operator manual now categorizes workflow tools into discovery,
draft workspace, stateless draft, artifact/deployment, and raw escape
hatch groups.
- Next: tighten the actual tool responses around that map so list calls stay
compact and inspect/run calls carry the detailed payloads.
3. **Wrapper creation ergonomics**
- Help create workflow-ready wrappers from raw capabilities.
@@ -37,6 +42,19 @@ on both the current docs and the implementation state.
and failure policy. Do not model this as plain parallel calls over sync
handlers.
Frame stress points to solve before either feature:
- `RunState.current_frame_id` currently models one active execution cursor.
Parallel foreach likely needs multiple runnable child frames.
- `ExecutionFrame.metadata` currently carries ad hoc foreach data. Subgraphs and
parallel foreach should get typed frame payloads or strongly bounded helper
accessors before metadata grows more meanings.
- Subgraph frames need child workflow identity/version/deployment binding, not
just a generic metadata dictionary.
- `RunState.current_node_id` duplicates the current frame's node id for
convenience. Any multi-frame scheduler must either keep that as a selected
cursor or replace it with an explicit scheduling view.
## Why This Order
`wf_core` is now coherent enough for the next bottleneck to be platform and DX:
+6
View File
@@ -92,6 +92,12 @@ limits and intended adapter seam.
- Saved workflow-as-node execution with interrupts requires a core runtime
upgrade: nested run state, child-frame trace preservation, interrupt bubbling
with path metadata, and resume back into the child workflow.
- Frames are currently a serial execution stack. That is enough for root
workflow execution, serial foreach, and node-level interrupts, but async
parallel foreach and native subgraphs will stress the model. In particular,
`RunState.current_frame_id` assumes one active cursor, `ExecutionFrame.metadata`
is ad hoc, and subgraph frames will need explicit child workflow/deployment
identity.
- Runtime errors are still ordinary exceptions plus failed run status. A richer
error payload can be added later, but should be designed as part of trace/run
state rather than scattered exceptions.
+153
View File
@@ -392,6 +392,11 @@ Expected shape:
}
```
Do not request trace detail by default. `run_deployment` returns `trace_count`
as the total original trace length and supports explicit ranged debug reads such
as `"trace_range": {"start": 0, "limit": 10}`. Trace entries may contain
resolved inputs, outputs, and state changes.
## 10. Rebind The Same Artifact Later
If another compatible account appears:
@@ -549,3 +554,151 @@ Concrete MCP sequence:
`create_wrapper_from_workspace` accepts the same request shape except there is
no `kind` field. It always saves `kind="wrapper"` and the result is discoverable
as a workflow capability named `workflow.<artifact_id>.v<version>`.
## Wrapper Happy Path
Use this path when a raw/provider capability is callable but you want a reusable
workflow-facing wrapper with explicit schemas, outcomes, and bindings.
### 1. Inspect The Source Capability
```yaml
tool: wf.workflow.inspect_capability
arguments:
{
"qualified_name": "demo.personal.echo_tool"
}
```
The response includes `wrapper_hints`. These hints are scaffolding, not final
business logic. They suggest draft schemas and basic input/output bindings.
### 2. Create A Draft Workspace From The Capability
```yaml
tool: wf.workflow.create_draft_workspace_from_capability
arguments:
{
"request": {
"workspace_id": "echo_wrapper_draft",
"capability_name": "demo.personal.echo_tool",
"name": "echo_wrapper",
"title": "Echo Wrapper Draft"
}
}
```
This creates a mutable, revisioned workspace using the inspected
`wrapper_hints`.
### 3. Patch Or Validate The Workspace
If the hints are good enough, validate:
```yaml
tool: wf.workflow.validate_draft_workspace
arguments:
{
"request": {
"workspace_id": "echo_wrapper_draft"
}
}
```
If one field is wrong, use a focused helper or JSON Patch. For example, change
one route:
```yaml
tool: wf.workflow.set_draft_route
arguments:
{
"request": {
"workspace_id": "echo_wrapper_draft",
"revision": 1,
"step_id": "echo",
"outcome": "error",
"target": "__end__"
}
}
```
### 4. Save The Workspace As A Wrapper Artifact
```yaml
tool: wf.workflow.create_wrapper_from_workspace
arguments:
{
"request": {
"workspace_id": "echo_wrapper_draft",
"artifact_id": "echo_wrapper",
"version": 1,
"title": "Echo Wrapper",
"outcomes": ["ok", "error"],
"source_bindings": {
"demo": "demo.personal"
}
}
}
```
The saved wrapper appears in workflow capability discovery as:
```text
workflow.echo_wrapper.v1
```
### 5. Deploy And Test The Wrapper
Saved wrappers that use logical sources need a deployment binding when called:
```yaml
tool: wf.workflow.save_deployment
arguments:
{
"deployment": {
"id": "echo_wrapper.personal",
"artifact_id": "echo_wrapper",
"artifact_version": 1,
"bindings": {
"demo": "demo.personal",
"wf.std": "wf.std"
}
}
}
```
Then test the wrapper through the workflow-facing REPL tool:
```yaml
tool: wf.workflow.call_capability
arguments:
{
"qualified_name": "workflow.echo_wrapper.v1",
"deployment_id": "echo_wrapper.personal",
"payload": {
"text": "hello"
}
}
```
Expected shape:
```json
{
"qualified_name": "workflow.echo_wrapper.v1",
"kind": "wrapper_artifact",
"outcome": "completed",
"output": {
"echoed": "hello"
},
"diagnostics": []
}
```
Current limitation: saved wrappers are executed through the deployment runtime,
so `call_capability` reports the wrapper run status (`completed`, `failed`, or
`interrupted`) rather than remapping inner node outcomes. True graph-as-node
outcome propagation belongs in `wf_core` subgraph support.
See `examples/mcp_wrapper_authoring_flow.py` for this same sequence through the
Python handler layer.
+99
View File
@@ -97,6 +97,96 @@ Typical tools:
- `wf.workflow.validate_deployment`
- `wf.workflow.run_deployment`
## Workflow Tool Map
The workflow surface is intentionally split by job. Use the primary path first;
the advanced tools exist for debugging, compatibility, or focused repair.
### Discovery
Primary:
- `wf.workflow.list_capabilities`: compact, paged workflow capability search.
It returns names, source ids, outcomes, and top-level field names.
- `wf.workflow.inspect_capability`: full contract for one selected capability,
including schemas, outcomes, and wrapper authoring hints.
- `wf.workflow.call_capability`: REPL-style direct test of one workflow
capability or saved wrapper artifact.
Supporting:
- `wf.workflow.list_artifacts`: compact list of saved workflow and wrapper
artifacts.
- `wf.workflow.inspect_artifact`: full saved artifact payload.
### Draft Workspaces
Primary:
- `wf.workflow.create_draft_workspace_from_capability`: preferred wrapper
bootstrap. It inspects one capability, applies its hints, and creates a
revisioned draft workspace.
- `wf.workflow.get_draft_workspace`: fetch current revision and optionally the
full draft document.
- `wf.workflow.validate_draft_workspace`: refresh diagnostics without changing
revision.
- `wf.workflow.create_wrapper_from_workspace`: save a validated draft workspace
as a callable wrapper capability.
- `wf.workflow.create_artifact_from_workspace`: save a validated draft workspace
as a full workflow artifact.
Focused repair helpers:
- `wf.workflow.set_draft_name`
- `wf.workflow.set_draft_route`
- `wf.workflow.set_step_input_map`
- `wf.workflow.set_step_output_map`
These helpers are deliberately narrow. Prefer them over JSON Patch when the
caller only needs to edit one common field.
Advanced workspace tools:
- `wf.workflow.list_draft_workspaces`: find mutable draft sessions.
- `wf.workflow.patch_draft_workspace`: apply RFC 6902 JSON Patch with revision
checking.
- `wf.workflow.delete_draft_workspace`: cleanup abandoned sessions.
- `wf.workflow.create_draft_workspace`: store a caller-provided draft directly.
- `wf.workflow.create_minimal_draft_workspace`: bootstrap around one capability
when the caller already knows schemas and bindings.
### Stateless Draft Tools
Use these when the caller can resend the whole draft on every call:
- `wf.workflow.validate_draft`
- `wf.workflow.compile_draft`
- `wf.workflow.patch_draft`
- `wf.workflow.create_artifact_from_draft`
Draft workspaces are usually safer for LLM clients because they avoid a
rewrite-the-whole-document loop and preserve optimistic-concurrency revisions.
### Artifact And Deployment
Primary:
- `wf.workflow.save_deployment`: bind one saved artifact version to concrete
sources.
- `wf.workflow.validate_deployment`: check dependency availability and drift.
- `wf.workflow.run_deployment`: execute a saved deployment with input. The
default response is compact and returns `trace_count`; pass `trace_range`
only when debugging a failed or surprising run.
Advanced:
- `wf.workflow.save_artifact`: persist a complete artifact JSON document.
- `wf.workflow.create_artifact_from_plan`: raw compiled-plan escape hatch.
`create_artifact_from_plan` bypasses draft ergonomics. Use it only when the
caller already has a trusted compiled raw workflow plan or is deliberately
testing the lower-level artifact boundary.
### `wf.docs`
Local documentation source.
@@ -253,6 +343,15 @@ Use `run_deployment` rather than expecting newly saved workflows to appear as
brand-new MCP tools. Many LLM harnesses do not reliably refresh callable tool
schemas mid-session.
The default `run_deployment` response is intentionally compact. It includes run
status, output, diagnostics, and `trace_count`, where `trace_count` is the total
number of trace entries in the original run. If the caller needs node-level
debug detail, pass an explicit `trace_range` object such as
`{"start": 0, "limit": 10}`; otherwise trace entries stay out of the normal
response. Trace entries may include resolved node inputs, node outputs, and
state changes, so treat them as debug payloads rather than ordinary list/summary
data.
## Which Tool Do I Use?
| I want to... | Use |
+1 -1
View File
@@ -6,7 +6,7 @@ from typing import Any
from wf_artifacts import WorkflowDeployment
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from mcp_workflow_surface import prepare_demo_service
from examples.mcp_workflow_surface import prepare_demo_service
async def author_echo_wrapper_from_capability(root: Path) -> dict[str, Any]:
+2 -1
View File
@@ -1,4 +1,5 @@
from .handlers import WorkflowSurfaceHandlers
from .models import TraceRange
from .tools import register_workflow_tools
__all__ = ["WorkflowSurfaceHandlers", "register_workflow_tools"]
__all__ = ["TraceRange", "WorkflowSurfaceHandlers", "register_workflow_tools"]
+32 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import asdict
from typing import TYPE_CHECKING, Any
from wf_artifacts import (
@@ -49,6 +50,7 @@ from .constants import (
DEFAULT_OK_OUTCOME,
RUNTIME_ERROR_CAPABILITY,
)
from .models import TraceRange
from .refs import parse_workflow_surface_capability_id
from .wrapper_hints import wrapper_hints_for_capability
@@ -862,6 +864,7 @@ class WorkflowSurfaceHandlers:
*,
deployment_id: str,
workflow_input: dict[str, Any],
trace_range: TraceRange | None = None,
) -> dict[str, Any]:
deployment, artifact, diagnostics = self._deployment_validation(deployment_id)
if diagnostics:
@@ -894,6 +897,22 @@ class WorkflowSurfaceHandlers:
status=run.status.value,
output=run.output,
trace_count=len(run.trace),
trace=(
[
asdict(entry)
for entry in run.trace[
trace_range.start : trace_range.start + trace_range.limit
]
]
if trace_range is not None
else None
),
trace_start=trace_range.start if trace_range is not None else None,
trace_limit=trace_range.limit if trace_range is not None else None,
trace_truncated=(
trace_range is not None
and len(run.trace) > trace_range.start + trace_range.limit
),
)
def _deployment_validation(
@@ -1219,8 +1238,12 @@ def _run_payload(
diagnostics: list[DependencyDiagnostic] | None = None,
output: dict[str, Any] | None = None,
trace_count: int = 0,
trace: list[dict[str, Any]] | None = None,
trace_start: int | None = None,
trace_limit: int | None = None,
trace_truncated: bool = False,
) -> dict[str, Any]:
return {
payload = {
"deployment_id": deployment.id,
"artifact_id": artifact.id,
"artifact_version": artifact.version,
@@ -1231,3 +1254,11 @@ def _run_payload(
],
"trace_count": trace_count,
}
if trace is not None:
# Trace entries can grow quickly, so the public run tool only includes
# a bounded debug slice when the caller explicitly asks for a range.
payload["trace_start"] = trace_start
payload["trace_limit"] = trace_limit
payload["trace"] = trace
payload["trace_truncated"] = trace_truncated
return payload
+16
View File
@@ -98,6 +98,22 @@ class CallCapabilityResult(BaseModel):
)
class TraceRange(BaseModel):
"""Bounded debug trace slice request for deployment runs."""
start: int = Field(
default=0,
ge=0,
description="Zero-based trace entry offset to start reading from.",
)
limit: int = Field(
default=25,
ge=1,
le=100,
description="Maximum trace entries to return. Keep this small.",
)
class DraftWorkspaceResult(BaseModel):
"""Inspector-visible response contract for draft workspace operations."""
+5 -1
View File
@@ -26,6 +26,7 @@ from .models import (
SetDraftRouteRequest,
SetStepInputMapRequest,
SetStepOutputMapRequest,
TraceRange,
ValidateDraftWorkspaceRequest,
)
@@ -538,14 +539,17 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
title="Run Workflow Deployment",
description=(
"Run deployment_id with workflow_input and return status, output, "
"diagnostics, and trace_count."
"diagnostics, and trace_count. Debug traces can include resolved "
"inputs and state changes; pass trace_range only when needed."
),
)
async def run_deployment(
deployment_id: str,
workflow_input: dict[str, Any],
trace_range: TraceRange | None = None,
) -> dict[str, Any]:
return await handlers.run_deployment(
deployment_id=deployment_id,
workflow_input=workflow_input,
trace_range=trace_range,
)
@@ -7,6 +7,7 @@ from examples.mcp_workflow_surface import (
create_and_run_echo_deployment,
prepare_demo_service,
)
from examples.mcp_wrapper_authoring_flow import author_echo_wrapper_from_capability
def test_mcp_workflow_surface_example_discovers_ok_and_error_outcomes(
@@ -35,3 +36,17 @@ def test_mcp_workflow_surface_example_runs_happy_path(tmp_path) -> None:
assert payload["status"] == "completed"
assert payload["output"]["echoed"] == "hello"
assert payload["diagnostics"] == []
def test_mcp_wrapper_authoring_flow_example_creates_and_calls_wrapper(tmp_path) -> (
None
):
payload = asyncio.run(author_echo_wrapper_from_capability(tmp_path))
assert payload["inspected_hints"]["capability_name"] == "demo.personal.echo_tool"
assert payload["workspace"]["status"] == "valid"
assert payload["created"]["saved"] is True
assert payload["called"]["qualified_name"] == "workflow.echo_wrapper.v1"
assert payload["called"]["kind"] == "wrapper_artifact"
assert payload["called"]["outcome"] == "completed"
assert payload["called"]["output"]["echoed"] == "hello"
+82 -1
View File
@@ -15,7 +15,7 @@ from wf_authoring import node, reducer
from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig, RawWorkflowPlan
from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from wf_mcp.workflow_surface import TraceRange, WorkflowSurfaceHandlers
from wf_core.models.steps import InputPathBinding, OutputBinding
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
from wf_platform import (
@@ -906,6 +906,87 @@ def test_workflow_surface_runs_non_interrupting_deployment() -> None:
assert payload["status"] == "completed"
assert payload["output"]["echoed"] == "hello"
assert payload["diagnostics"] == []
assert payload["trace_count"] == 1
assert "trace" not in payload
def test_workflow_surface_run_deployment_can_include_trace_detail() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_run_trace_detail"
)
artifact_store.save_artifact(_echo_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_run_trace_detail_mcp"),
artifact_store=artifact_store,
)
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.run_deployment(
deployment_id="echo.personal",
workflow_input={"text": "hello"},
trace_range=TraceRange(start=0, limit=10),
)
)
assert payload["status"] == "completed"
assert payload["trace_count"] == 1
assert payload["trace_start"] == 0
assert payload["trace_limit"] == 10
assert payload["trace_truncated"] is False
assert len(payload["trace"]) == 1
assert payload["trace"][0]["node_id"] == "echo"
assert payload["trace"][0]["outcome"] == "ok"
def test_workflow_surface_run_deployment_can_read_empty_trace_range() -> None:
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_run_trace_empty_range"
)
artifact_store.save_artifact(_echo_artifact())
artifact_store.save_deployment(
WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
)
service = WfMcpService(
store=FileStore(local_temp_root() / "surface_run_trace_empty_range_mcp"),
artifact_store=artifact_store,
)
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.run_deployment(
deployment_id="echo.personal",
workflow_input={"text": "hello"},
trace_range=TraceRange(start=5, limit=10),
)
)
assert payload["trace_count"] == 1
assert payload["trace_start"] == 5
assert payload["trace_limit"] == 10
assert payload["trace"] == []
assert payload["trace_truncated"] is False
def test_workflow_surface_runs_deployment_with_bound_node_spec_dependency() -> None: