list deployment compaction & inspect deployment

This commit is contained in:
lda
2026-05-21 18:44:16 +07:00 Verified
parent abf41677a6
commit 884416dfad
10 changed files with 188 additions and 43 deletions
+44 -26
View File
@@ -1,46 +1,62 @@
# Current Roadmap # Current Roadmap
This is the short active roadmap after the core type-shape cleanup. It is based This is the short active roadmap after the core type-shape cleanup and MCP
on both the current docs and the implementation state. workflow authoring cleanup pass. It is based on both the current docs and the
implementation state.
## Next Work ## Completed Cleanup Pass
1. **Docs index and prune** 1. **Docs index and prune**
- Add/maintain a clear docs entry point. - Current architecture docs are separated from historical plans and scratch
- Keep current architecture docs separate from historical plans and scratch
notes. notes.
- The active roadmap now lives here instead of being scattered through older
planning files.
2. **MCP workflow authoring UX** 2. **MCP workflow authoring UX**
- Make the LLM/client path progressive: inspect sources, create a draft, - The operator manual categorizes workflow tools into discovery, draft
patch, validate, compile, save, run. workspace, stateless draft, artifact/deployment, run/debug, and raw escape
- 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. hatch groups.
- Next: tighten the actual tool responses around that map so list calls stay - List-style tools are more compact, while inspect/run tools carry the
compact and inspect/run calls carry the detailed payloads. detailed payloads.
3. **Wrapper creation ergonomics** 3. **Wrapper creation ergonomics**
- Help create workflow-ready wrappers from raw capabilities. - Wrapper draft helpers can suggest state schema, input bindings, output
- Suggest state schema, input bindings, output bindings, default `ok` / bindings, default `ok` / `error` handling, and missing decisions.
`error` handling, and missing decisions. - The end-to-end runbook documents the wrapper path from capability
discovery through deployment/run.
4. **Run and deployment story** 4. **Run and deployment story**
- Tighten list/inspect/run/debug for artifacts and deployments. - Deployment listing is summary-first, with dedicated inspection for detail.
- Keep dependency validation and trace/error output compact and actionable. - `run_deployment` returns compact status by default and exposes trace slices
through an explicit `trace_range`.
- Dependency validation and error output remain part of the run path.
5. **Source inventory polish** 5. **Source inventory polish**
- Make `list_sources` / `inspect_source` clearly show raw capabilities, - `list_sources` / `inspect_source` now present source-owned capabilities
workflow-ready node specs, admin-only tools, docs/resources, enabled state, progressively.
and changes after reload. - Source inventory distinguishes external sources, local workflow-facing
sources, docs/resources, and admin-only control surfaces.
## Runtime Work To Revisit Later ## Runtime and Platform Roadmap
- **Native subgraphs**: add child run state, child trace preservation, interrupt - **Native subgraphs / graph-as-node**: add child run state, child trace
bubbling, and resume back into the child workflow. preservation, interrupt bubbling, and resume back into the child workflow.
Wrapper artifacts currently execute as deployments and return run status;
true graph-as-node outcome propagation belongs here.
- **Async parallel foreach**: add explicit scheduling, reducer/merge semantics, - **Async parallel foreach**: add explicit scheduling, reducer/merge semantics,
and failure policy. Do not model this as plain parallel calls over sync and failure policy. Do not model this as plain parallel calls over sync
handlers. handlers.
- **Persistent run history**: add a run store before adding stable `run_id`,
`inspect_run`, or `read_run_trace(run_id, range)` APIs. Current traces are
returned directly from immediate run responses.
- **Protocol-native long-running runs**: investigate MCP tasks/progress
notifications for long-running workflow execution. Avoid inventing a custom
"start" convention unless protocol-native behavior is insufficient.
- **Dynamic saved workflows as tools**: defer until the stable run/inspect
surface is strong. Many MCP clients do not refresh tool lists reliably, so
`wf.workflow.run_deployment` remains the dependable front door.
- **Dashboard/source controls**: future UI should consume the same source
inventory and deployment metadata instead of reverse-engineering MCP tools.
Frame stress points to solve before either feature: Frame stress points to solve before either feature:
@@ -57,6 +73,8 @@ Frame stress points to solve before either feature:
## Why This Order ## Why This Order
`wf_core` is now coherent enough for the next bottleneck to be platform and DX: The MCP workflow authoring path is now usable enough for real testing. The next
how a human or LLM discovers capabilities, turns them into workflow-ready bottleneck is runtime/platform correctness: resumable child execution,
pieces, saves them, and runs them again. parallel scheduling, persistent run history, and protocol-native progress
reporting. Those pieces should come before adding more high-level authoring
sugar.
+8
View File
@@ -118,6 +118,9 @@ Supporting:
- `wf.workflow.list_artifacts`: compact list of saved workflow and wrapper - `wf.workflow.list_artifacts`: compact list of saved workflow and wrapper
artifacts. artifacts.
- `wf.workflow.inspect_artifact`: full saved artifact payload. - `wf.workflow.inspect_artifact`: full saved artifact payload.
- `wf.workflow.list_deployments`: compact list of saved deployment summaries.
- `wf.workflow.inspect_deployment`: full deployment payload including source
bindings.
### Draft Workspaces ### Draft Workspaces
@@ -173,6 +176,8 @@ Primary:
- `wf.workflow.save_deployment`: bind one saved artifact version to concrete - `wf.workflow.save_deployment`: bind one saved artifact version to concrete
sources. sources.
- `wf.workflow.inspect_deployment`: inspect source bindings for one saved
deployment.
- `wf.workflow.validate_deployment`: check dependency availability and drift. - `wf.workflow.validate_deployment`: check dependency availability and drift.
- `wf.workflow.run_deployment`: execute a saved deployment with input. The - `wf.workflow.run_deployment`: execute a saved deployment with input. The
default response is compact and returns `trace_count`; pass `trace_range` default response is compact and returns `trace_count`; pass `trace_range`
@@ -372,6 +377,9 @@ data.
| Save a workflow definition from a draft | `wf.workflow.create_artifact_from_draft` | | Save a workflow definition from a draft | `wf.workflow.create_artifact_from_draft` |
| Save a compiled raw workflow definition | `wf.workflow.create_artifact_from_plan` | | Save a compiled raw workflow definition | `wf.workflow.create_artifact_from_plan` |
| List saved workflows/wrappers | `wf.workflow.list_artifacts` | | List saved workflows/wrappers | `wf.workflow.list_artifacts` |
| Inspect one saved workflow/wrapper | `wf.workflow.inspect_artifact` |
| List saved deployments | `wf.workflow.list_deployments` |
| Inspect one saved deployment | `wf.workflow.inspect_deployment` |
| Bind a saved workflow to concrete sources | `wf.workflow.save_deployment` | | Bind a saved workflow to concrete sources | `wf.workflow.save_deployment` |
| Check whether a deployment can run | `wf.workflow.validate_deployment` | | Check whether a deployment can run | `wf.workflow.validate_deployment` |
| Execute a saved workflow | `wf.workflow.run_deployment` | | Execute a saved workflow | `wf.workflow.run_deployment` |
+15
View File
@@ -69,6 +69,21 @@ semantics.
Dynamic projection of saved workflows as individual MCP tools can exist later, Dynamic projection of saved workflows as individual MCP tools can exist later,
but it should be optional. The stable run tool is the reliable base layer. but it should be optional. The stable run tool is the reliable base layer.
Current `run_deployment` calls are synchronous request/response executions. They
return compact status, output, diagnostics, and `trace_count`; optional ranged
trace detail is for debugging only.
Future run history should introduce a stable `run_id` only when there is a real
run store behind it. A `run_id` without persisted state, trace paging, and
status lookup would be misleading. The likely shape is:
- `run_deployment` starts or completes a run and returns `run_id`
- `inspect_run(run_id)` returns status, output, diagnostics, and trace metadata
- `read_run_trace(run_id, range)` returns bounded trace slices
Until that exists, clients should treat the current response as the complete
ephemeral run result for this request.
For long-running workflow execution, prefer MCP-native execution mechanisms For long-running workflow execution, prefer MCP-native execution mechanisms
where available: where available:
+34 -4
View File
@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Annotated, Any
from pydantic import Field
from wf_mcp.broker.service import WfMcpService from wf_mcp.broker.service import WfMcpService
@@ -84,8 +86,26 @@ def register_service_admin_tools(
description="List compact configured capability source summaries.", description="List compact configured capability source summaries.",
) )
async def list_sources( async def list_sources(
cursor: str | None = None, cursor: Annotated[
limit: int = 50, str | None,
Field(
description=(
"Opaque pagination cursor returned by a previous list_sources "
"call. Omit for the first page."
)
),
] = None,
limit: Annotated[
int,
Field(
ge=1,
le=100,
description=(
"Maximum source summaries to return. Use inspect_source for "
"one full source inventory."
),
),
] = 50,
) -> dict[str, Any]: ) -> dict[str, Any]:
return handlers.list_sources(cursor=cursor, limit=limit) return handlers.list_sources(cursor=cursor, limit=limit)
@@ -94,7 +114,17 @@ def register_service_admin_tools(
title="Inspect Source", title="Inspect Source",
description="Return the full inventory for one configured capability source.", description="Return the full inventory for one configured capability source.",
) )
async def inspect_source(source_id: str) -> dict[str, Any]: async def inspect_source(
source_id: Annotated[
str,
Field(
description=(
"Exact source id from list_sources, such as wf.std, wf.docs, "
"or an enabled connection id like demo.personal."
)
),
],
) -> dict[str, Any]:
return handlers.inspect_source(source_id) return handlers.inspect_source(source_id)
@server.tool( @server.tool(
+1
View File
@@ -55,6 +55,7 @@ _SEARCH_ALWAYS_VISIBLE_TOOL_NAMES = [
"wf.workflow.create_wrapper_from_workspace", "wf.workflow.create_wrapper_from_workspace",
"wf.workflow.inspect_artifact", "wf.workflow.inspect_artifact",
"wf.workflow.list_deployments", "wf.workflow.list_deployments",
"wf.workflow.inspect_deployment",
"wf.workflow.save_deployment", "wf.workflow.save_deployment",
"wf.workflow.validate_deployment", "wf.workflow.validate_deployment",
"wf.workflow.run_deployment", "wf.workflow.run_deployment",
+19 -1
View File
@@ -819,11 +819,18 @@ class WorkflowSurfaceHandlers:
return {"deployments": []} return {"deployments": []}
return { return {
"deployments": [ "deployments": [
deployment.model_dump(mode="json") _deployment_summary(deployment)
for deployment in self.service.artifact_store.list_deployments() for deployment in self.service.artifact_store.list_deployments()
] ]
} }
async def inspect_deployment(self, *, deployment_id: str) -> dict[str, Any]:
if self.service.artifact_store is None:
raise KeyError("workflow artifact store is not configured")
return self.service.artifact_store.get_deployment(deployment_id).model_dump(
mode="json"
)
async def save_deployment(self, deployment: dict[str, Any]) -> dict[str, Any]: async def save_deployment(self, deployment: dict[str, Any]) -> dict[str, Any]:
if self.service.artifact_store is None: if self.service.artifact_store is None:
raise KeyError("workflow artifact store is not configured") raise KeyError("workflow artifact store is not configured")
@@ -1262,3 +1269,14 @@ def _run_payload(
payload["trace"] = trace payload["trace"] = trace
payload["trace_truncated"] = trace_truncated payload["trace_truncated"] = trace_truncated
return payload return payload
def _deployment_summary(deployment: WorkflowDeployment) -> dict[str, Any]:
"""Return compact deployment metadata for progressive list responses."""
return {
"id": deployment.id,
"artifact_id": deployment.artifact_id,
"artifact_version": deployment.artifact_version,
"binding_count": len(deployment.binding_map()),
"drift_policy": deployment.drift_policy.value,
}
+12 -1
View File
@@ -514,11 +514,22 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
@server.tool( @server.tool(
name="wf.workflow.list_deployments", name="wf.workflow.list_deployments",
title="List Workflow Deployments", title="List Workflow Deployments",
description="List saved workflow deployments and their source bindings.", description=(
"List compact saved workflow deployment summaries. Use "
"inspect_deployment for full source bindings."
),
) )
async def list_deployments() -> dict[str, Any]: async def list_deployments() -> dict[str, Any]:
return await handlers.list_deployments() return await handlers.list_deployments()
@server.tool(
name="wf.workflow.inspect_deployment",
title="Inspect Workflow Deployment",
description="Return one full workflow deployment including source bindings.",
)
async def inspect_deployment(deployment_id: str) -> dict[str, Any]:
return await handlers.inspect_deployment(deployment_id=deployment_id)
@server.tool( @server.tool(
name="wf.workflow.save_deployment", name="wf.workflow.save_deployment",
title="Save Workflow Deployment", title="Save Workflow Deployment",
+19 -11
View File
@@ -10,6 +10,7 @@ from wf_platform.refs import CapabilityRef
SourceKind = Literal["system", "connection"] SourceKind = Literal["system", "connection"]
JsonObject = dict[str, Any] JsonObject = dict[str, Any]
SOURCE_PREVIEW_LIMIT = 3
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -143,7 +144,6 @@ class CapabilitySource:
def as_status(self) -> SourceStatus: def as_status(self) -> SourceStatus:
"""Return serializable source metadata without owned capability names.""" """Return serializable source metadata without owned capability names."""
preview_limit = 3
return SourceStatus( return SourceStatus(
id=self.id, id=self.id,
kind=self.kind, kind=self.kind,
@@ -166,21 +166,29 @@ class CapabilitySource:
prompt_count=len(self.capabilities.prompts), prompt_count=len(self.capabilities.prompts),
resource_count=len(self.capabilities.resources), resource_count=len(self.capabilities.resources),
preview=SourceCapabilityPreview( preview=SourceCapabilityPreview(
tools=_preview_names(self.capabilities.tools, preview_limit), tools=_preview_names(self.capabilities.tools, SOURCE_PREVIEW_LIMIT),
node_specs=_preview_names( node_specs=_preview_names(
self.capabilities.node_specs, self.capabilities.node_specs,
preview_limit, SOURCE_PREVIEW_LIMIT,
),
reducers=_preview_names(
self.capabilities.reducers, SOURCE_PREVIEW_LIMIT
),
prompts=_preview_names(
self.capabilities.prompts, SOURCE_PREVIEW_LIMIT
),
resources=_preview_names(
self.capabilities.resources, SOURCE_PREVIEW_LIMIT
), ),
reducers=_preview_names(self.capabilities.reducers, preview_limit),
prompts=_preview_names(self.capabilities.prompts, preview_limit),
resources=_preview_names(self.capabilities.resources, preview_limit),
), ),
has_more=SourceCapabilityHasMore( has_more=SourceCapabilityHasMore(
tools=_has_more(self.capabilities.tools, preview_limit), tools=_has_more(self.capabilities.tools, SOURCE_PREVIEW_LIMIT),
node_specs=_has_more(self.capabilities.node_specs, preview_limit), node_specs=_has_more(
reducers=_has_more(self.capabilities.reducers, preview_limit), self.capabilities.node_specs, SOURCE_PREVIEW_LIMIT
prompts=_has_more(self.capabilities.prompts, preview_limit), ),
resources=_has_more(self.capabilities.resources, preview_limit), reducers=_has_more(self.capabilities.reducers, SOURCE_PREVIEW_LIMIT),
prompts=_has_more(self.capabilities.prompts, SOURCE_PREVIEW_LIMIT),
resources=_has_more(self.capabilities.resources, SOURCE_PREVIEW_LIMIT),
), ),
) )
+9
View File
@@ -132,6 +132,14 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
assert create_workspace_schema is not None assert create_workspace_schema is not None
assert "workspace_id" in create_workspace_schema["properties"] assert "workspace_id" in create_workspace_schema["properties"]
assert "revision" in create_workspace_schema["properties"] assert "revision" in create_workspace_schema["properties"]
list_sources_schema = tools_by_name["wf.admin.list_sources"].inputSchema
assert "inspect_source" in list_sources_schema["properties"]["limit"][
"description"
]
inspect_source_schema = tools_by_name["wf.admin.inspect_source"].inputSchema
assert "Exact source id" in inspect_source_schema["properties"][
"source_id"
]["description"]
minimal_workspace_input = tools_by_name[ minimal_workspace_input = tools_by_name[
"wf.workflow.create_minimal_draft_workspace" "wf.workflow.create_minimal_draft_workspace"
].inputSchema ].inputSchema
@@ -274,6 +282,7 @@ def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
assert "wf.workflow.create_wrapper_from_workspace" in names assert "wf.workflow.create_wrapper_from_workspace" in names
assert "wf.workflow.inspect_artifact" in names assert "wf.workflow.inspect_artifact" in names
assert "wf.workflow.list_deployments" in names assert "wf.workflow.list_deployments" in names
assert "wf.workflow.inspect_deployment" in names
assert "wf.workflow.save_deployment" in names assert "wf.workflow.save_deployment" in names
assert "wf.workflow.validate_deployment" in names assert "wf.workflow.validate_deployment" in names
assert "wf.workflow.run_deployment" in names assert "wf.workflow.run_deployment" in names
+27
View File
@@ -268,6 +268,33 @@ def test_workflow_surface_records_artifact_and_deployment_save_events() -> None:
assert events[1].capability_id == "deployment.echo.personal" assert events[1].capability_id == "deployment.echo.personal"
def test_workflow_surface_lists_compact_deployment_summaries_and_inspects_detail() -> (
None
):
artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_deployment_list"
)
handlers = _handlers(artifact_store)
deployment = WorkflowDeployment(
id="echo.personal",
artifact_id="echo",
artifact_version=1,
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
)
artifact_store.save_deployment(deployment)
listed = asyncio.run(handlers.list_deployments())
inspected = asyncio.run(
handlers.inspect_deployment(deployment_id="echo.personal")
)
assert listed["deployments"][0]["id"] == "echo.personal"
assert listed["deployments"][0]["binding_count"] == 1
assert "bindings" not in listed["deployments"][0]
assert inspected["bindings"][0]["logical_source"] == "demo"
assert inspected["bindings"][0]["concrete_source"] == "demo.personal"
def test_workflow_surface_creates_wrapper_artifact_from_plan() -> None: def test_workflow_surface_creates_wrapper_artifact_from_plan() -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_wrapper_plan" local_temp_root() / "surface_wrapper_plan"