feat: add semantic draft authoring operations
This commit is contained in:
@@ -63,16 +63,17 @@ clear operator feedback before adding more architecture.
|
|||||||
- Completed: `wf schema` now lists workflow document/component models, emits
|
- Completed: `wf schema` now lists workflow document/component models, emits
|
||||||
compact JSON outlines for agent discovery, and emits valid self-contained
|
compact JSON outlines for agent discovery, and emits valid self-contained
|
||||||
JSON Schema with `--verbose`.
|
JSON Schema with `--verbose`.
|
||||||
- Completed: `wf draft add-state-from-output` projects capability output
|
|
||||||
property schemas into draft state schemas, preserving `$defs` / `definitions`
|
|
||||||
for schema refs and reducing brittle whole-`state_schema` patches.
|
|
||||||
- Completed: `wf draft bind-output-to-state` composes state schema projection
|
- Completed: `wf draft bind-output-to-state` composes state schema projection
|
||||||
with output binding merge, reducing manual draft patch repairs in agent
|
with output binding merge, reducing manual draft patch repairs in agent
|
||||||
challenge runs.
|
challenge runs.
|
||||||
- Completed: `wf draft add-step-from-capability` inserts one explicit
|
- Completed: `wf draft add-step-from-capability` inserts one explicit
|
||||||
capability-backed step with route, input, and output-to-state schema/binding
|
capability-backed step with route, input, and output-to-state schema/binding
|
||||||
wiring in a single revision, reducing brittle JSON Patch authoring for
|
wiring in a single revision, reducing brittle JSON Patch authoring for
|
||||||
multi-step workflows.
|
multi-step workflows. Accepts `--route OUTCOME=TARGET` for multi-outcome steps.
|
||||||
|
- Completed: `wf draft branch` and `wf draft handle` provide atomic route
|
||||||
|
editing for existing draft steps without rewriting the full routes object.
|
||||||
|
- Completed: `wf draft compile` returns the compiled raw plan plus required
|
||||||
|
capabilities without mutating or saving the draft workspace.
|
||||||
- Completed: draft validation now preserves structured core validation issues
|
- Completed: draft validation now preserves structured core validation issues
|
||||||
and adds exact `bind-output-to-state` repair hints for missing state fields.
|
and adds exact `bind-output-to-state` repair hints for missing state fields.
|
||||||
- Keep status read-only; do not mutate registry, auth, config, or stores.
|
- Keep status read-only; do not mutate registry, auth, config, or stores.
|
||||||
|
|||||||
+31
-10
@@ -297,7 +297,9 @@ wf draft set-route concat_ws --revision 2 --step call --outcome ok --to __end__
|
|||||||
wf draft set-input concat_ws --revision 3 --step call --map input.items=items --map input.separator=separator
|
wf draft set-input concat_ws --revision 3 --step call --map input.items=items --map input.separator=separator
|
||||||
wf draft set-output concat_ws --revision 4 --step call --map value=state.value
|
wf draft set-output concat_ws --revision 4 --step call --map value=state.value
|
||||||
wf draft set-input concat_ws --revision 5 --step call --merge --map input.limit=limit
|
wf draft set-input concat_ws --revision 5 --step call --merge --map input.limit=limit
|
||||||
wf draft add-state-from-output concat_ws --revision 5 --step call --output value --state state.value
|
wf draft branch concat_ws --revision 6 --step call --route ok=__end__ --route error=tool_error
|
||||||
|
wf draft handle concat_ws --revision 7 --to fail --branch lookup:error --branch transform:error
|
||||||
|
wf draft compile concat_ws
|
||||||
```
|
```
|
||||||
|
|
||||||
`set-input` maps graph source paths to node-local input fields:
|
`set-input` maps graph source paths to node-local input fields:
|
||||||
@@ -311,13 +313,6 @@ Use repeated `--map` flags in one command when you know the complete map. Use
|
|||||||
`--merge` when adding or updating one entry across a later revision while
|
`--merge` when adding or updating one entry across a later revision while
|
||||||
preserving existing bindings.
|
preserving existing bindings.
|
||||||
|
|
||||||
Before mapping a step output to a new state field, the state schema must declare
|
|
||||||
that root field. `add-state-from-output` copies the selected step capability's
|
|
||||||
top-level output property schema into `state_schema.properties`, including local
|
|
||||||
`$defs` / `definitions` blocks needed by `$ref` schemas. It only declares the
|
|
||||||
state field; still run `set-output` or `draft patch` to write values into that
|
|
||||||
field, then run `wf draft validate`.
|
|
||||||
|
|
||||||
### Bind A Step Output To State
|
### Bind A Step Output To State
|
||||||
|
|
||||||
Use `bind-output-to-state` when a step output should become workflow state and
|
Use `bind-output-to-state` when a step output should become workflow state and
|
||||||
@@ -351,8 +346,8 @@ wf draft add-step-from-capability report_ws \
|
|||||||
--capability local.report.render_markdown_report \
|
--capability local.report.render_markdown_report \
|
||||||
--from-step extract \
|
--from-step extract \
|
||||||
--from-outcome ok \
|
--from-outcome ok \
|
||||||
--outcome ok \
|
--route ok=__end__ \
|
||||||
--to __end__ \
|
--route error=tool_error \
|
||||||
--input state.title=title \
|
--input state.title=title \
|
||||||
--bind-output markdown=state.markdown
|
--bind-output markdown=state.markdown
|
||||||
```
|
```
|
||||||
@@ -360,6 +355,32 @@ wf draft add-step-from-capability report_ws \
|
|||||||
Run `wf draft validate report_ws` after adding the step. If validation returns
|
Run `wf draft validate report_ws` after adding the step. If validation returns
|
||||||
a `repair_hint`, prefer the focused helper in that hint before JSON Patch.
|
a `repair_hint`, prefer the focused helper in that hint before JSON Patch.
|
||||||
|
|
||||||
|
### Branch And Handle Existing Steps
|
||||||
|
|
||||||
|
Use `wf draft branch` to update routes for an existing step in one revision:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft branch concat_ws --revision 6 --step call --route ok=__end__ --route error=tool_error
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `wf draft handle` to route multiple source step outcomes to a common target:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft handle concat_ws --revision 7 --to fail --branch lookup:error --branch transform:error
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile A Draft Workspace
|
||||||
|
|
||||||
|
Use `wf draft compile` to return the compiled raw plan plus required
|
||||||
|
capabilities without mutating or saving the draft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft compile concat_ws
|
||||||
|
```
|
||||||
|
|
||||||
|
On success, prints the compiled plan to stdout. On invalid draft status, prints
|
||||||
|
the structured diagnostic envelope to stderr and exits nonzero.
|
||||||
|
|
||||||
Validate:
|
Validate:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -614,6 +614,9 @@ The workflow MCP surface exposes these draft tools:
|
|||||||
| `wf.workflow.compile_draft` | Return the compiled raw plan plus dependency summaries. |
|
| `wf.workflow.compile_draft` | Return the compiled raw plan plus dependency summaries. |
|
||||||
| `wf.workflow.patch_draft` | Apply RFC 6902 JSON Patch and validate the patched draft. |
|
| `wf.workflow.patch_draft` | Apply RFC 6902 JSON Patch and validate the patched draft. |
|
||||||
| `wf.workflow.create_artifact_from_draft` | Compile, normalize, and save a workflow artifact. |
|
| `wf.workflow.create_artifact_from_draft` | Compile, normalize, and save a workflow artifact. |
|
||||||
|
| `wf.workflow.branch_draft` | Update routes for an existing step in one revision. |
|
||||||
|
| `wf.workflow.handle_draft` | Route multiple source step outcomes to a common target. |
|
||||||
|
| `wf.workflow.compile_draft_workspace` | Return compiled plan plus capabilities without mutation. |
|
||||||
|
|
||||||
Use `validate_draft` before saving. Use `patch_draft` when an LLM client needs a
|
Use `validate_draft` before saving. Use `patch_draft` when an LLM client needs a
|
||||||
small targeted correction instead of rewriting the whole workflow.
|
small targeted correction instead of rewriting the whole workflow.
|
||||||
@@ -665,6 +668,9 @@ wf draft set-name <workspace_id> --revision <n> --name <name>
|
|||||||
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target_step_or___end__>
|
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target_step_or___end__>
|
||||||
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
|
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
|
||||||
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
||||||
|
wf draft branch <workspace_id> --revision <n> --step <step_id> --route ok=__end__ --route error=tool_error
|
||||||
|
wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error
|
||||||
|
wf draft compile <workspace_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `draft patch` when these focused commands do not cover the structural edit.
|
Use `draft patch` when these focused commands do not cover the structural edit.
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.te
|
|||||||
wf draft set-input <workspace_id> --revision <n> --step <step_id> --merge --map input.other=other
|
wf draft set-input <workspace_id> --revision <n> --step <step_id> --merge --map input.other=other
|
||||||
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
||||||
wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other
|
wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other
|
||||||
wf draft add-state-from-output <workspace_id> --revision <n> --step <step_id> --output <field> --state state.<field>
|
wf draft branch <workspace_id> --revision <n> --step <step_id> --route ok=__end__ --route error=fail
|
||||||
|
wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error
|
||||||
|
wf draft compile <workspace_id>
|
||||||
wf draft bind-output-to-state <workspace_id> --revision <n> --step <step_id> --output <field> --state state.<field>
|
wf draft bind-output-to-state <workspace_id> --revision <n> --step <step_id> --output <field> --state state.<field>
|
||||||
wf draft add-step-from-capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --outcome ok --to <next-or-__end__> --input input.text=text --bind-output result=state.result
|
wf draft add-step-from-capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
|
||||||
wf draft validate <workspace_id>
|
wf draft validate <workspace_id>
|
||||||
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
||||||
|
|
||||||
@@ -80,14 +82,9 @@ For `draft set-input` and `draft set-output`, repeated `--map` flags in one
|
|||||||
command define the complete replacement map. If you split map edits across
|
command define the complete replacement map. If you split map edits across
|
||||||
multiple commands, pass `--merge` or the later command replaces the earlier map.
|
multiple commands, pass `--merge` or the later command replaces the earlier map.
|
||||||
|
|
||||||
If mapping `LOCAL_SOURCE=state.new_field`, declare the state field first with
|
|
||||||
`draft add-state-from-output` when the schema should match a capability output
|
|
||||||
field. Do not hand-copy `$defs` unless the helper cannot express the shape.
|
|
||||||
|
|
||||||
Prefer `draft bind-output-to-state` when a step output should write to a new
|
Prefer `draft bind-output-to-state` when a step output should write to a new
|
||||||
root state field. It declares the matching state schema and merges the output
|
root state field. It declares the matching state schema and merges the output
|
||||||
binding in one revision-checked edit. Use `draft add-state-from-output` only
|
binding in one revision-checked edit.
|
||||||
when you need the schema declaration without changing bindings.
|
|
||||||
`bind-output-to-state` requires a capability-backed step with `use`; use JSON
|
`bind-output-to-state` requires a capability-backed step with `use`; use JSON
|
||||||
Patch for non-capability/control draft steps.
|
Patch for non-capability/control draft steps.
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,11 @@ Prefer focused helpers over JSON Patch for common edits:
|
|||||||
- `set_draft_route`
|
- `set_draft_route`
|
||||||
- `set_step_input_map`
|
- `set_step_input_map`
|
||||||
- `set_step_output_map`
|
- `set_step_output_map`
|
||||||
- `add_state_schema_from_output`
|
|
||||||
- `bind_output_to_state`
|
- `bind_output_to_state`
|
||||||
- `add_step_from_capability`
|
- `add_step_from_capability`
|
||||||
|
- `branch_draft`
|
||||||
|
- `handle_draft`
|
||||||
|
- `compile_draft_workspace`
|
||||||
|
|
||||||
CLI equivalents:
|
CLI equivalents:
|
||||||
|
|
||||||
@@ -86,9 +88,11 @@ wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.te
|
|||||||
wf draft set-input <workspace_id> --revision <n> --step <step_id> --merge --map input.other=other
|
wf draft set-input <workspace_id> --revision <n> --step <step_id> --merge --map input.other=other
|
||||||
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
|
||||||
wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other
|
wf draft set-output <workspace_id> --revision <n> --step <step_id> --merge --map other=state.other
|
||||||
wf draft add-state-from-output <workspace_id> --revision <n> --step <step_id> --output <field> --state state.<field>
|
wf draft branch <workspace_id> --revision <n> --step <step_id> --route ok=__end__ --route error=fail
|
||||||
|
wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --branch transform:error
|
||||||
|
wf draft compile <workspace_id>
|
||||||
wf draft bind-output-to-state <workspace_id> --revision <n> --step <step_id> --output <field> --state state.<field>
|
wf draft bind-output-to-state <workspace_id> --revision <n> --step <step_id> --output <field> --state state.<field>
|
||||||
wf draft add-step-from-capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --outcome ok --to <next-or-__end__> --input input.text=text --bind-output result=state.result
|
wf draft add-step-from-capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
|
||||||
```
|
```
|
||||||
|
|
||||||
`set-input` direction: `input.text=text` means graph source `input.text` maps to
|
`set-input` direction: `input.text=text` means graph source `input.text` maps to
|
||||||
@@ -101,10 +105,6 @@ Without `--merge`, `set-input` and `set-output` replace the whole map for that
|
|||||||
step. Use repeated `--map` flags in one command for a complete replacement. Use
|
step. Use repeated `--map` flags in one command for a complete replacement. Use
|
||||||
`--merge` only when adding/updating entries over multiple revisions.
|
`--merge` only when adding/updating entries over multiple revisions.
|
||||||
|
|
||||||
Use `add-state-from-output` when the target state field should reuse a capability
|
|
||||||
output schema. This prevents dangling `$ref` values by copying local `$defs` /
|
|
||||||
`definitions` with the selected property schema.
|
|
||||||
|
|
||||||
- `bind_output_to_state`
|
- `bind_output_to_state`
|
||||||
|
|
||||||
Declares one root state field from a step capability output schema and merges
|
Declares one root state field from a step capability output schema and merges
|
||||||
@@ -124,15 +124,35 @@ wf draft validate <workspace_id>
|
|||||||
|
|
||||||
Adds a new capability-backed step with explicit route, input bindings, and
|
Adds a new capability-backed step with explicit route, input bindings, and
|
||||||
output-to-state schema/binding wiring in one revision. It can set the incoming
|
output-to-state schema/binding wiring in one revision. It can set the incoming
|
||||||
edge, outgoing edge, input map, and output-to-state schema/binding. It still
|
edge, outgoing edges, input map, and output-to-state schema/binding. Use
|
||||||
|
`--route OUTCOME=TARGET` for each outcome; when omitted and the capability
|
||||||
|
declares a single outcome, that outcome routes to `__end__`. It still
|
||||||
requires explicit choices; if you do not know a map, inspect the capability or
|
requires explicit choices; if you do not know a map, inspect the capability or
|
||||||
run validation rather than guessing.
|
run validation rather than guessing.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wf draft add-step-from-capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --outcome ok --to <next-or-__end__> --input input.text=text --bind-output result=state.result
|
wf draft add-step-from-capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
|
||||||
wf draft validate <workspace_id>
|
wf draft validate <workspace_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `branch_draft`
|
||||||
|
|
||||||
|
Updates routes for an existing step in one revision without rewriting the
|
||||||
|
full routes object. Supply `--route OUTCOME=TARGET` for each outcome to
|
||||||
|
set or update.
|
||||||
|
|
||||||
|
- `handle_draft`
|
||||||
|
|
||||||
|
Routes multiple source step outcomes to a common target. Supply
|
||||||
|
`--branch STEP:OUTCOME` for each source outcome and `--to TARGET` for the
|
||||||
|
shared destination.
|
||||||
|
|
||||||
|
- `compile_draft_workspace`
|
||||||
|
|
||||||
|
Returns the compiled raw plan plus required capabilities without mutating
|
||||||
|
or saving the draft workspace. On invalid draft status, returns structured
|
||||||
|
diagnostics without a `compiled_plan`.
|
||||||
|
|
||||||
Validation repair hints are product guidance. If a diagnostic suggests
|
Validation repair hints are product guidance. If a diagnostic suggests
|
||||||
`bind-output-to-state`, use it before hand-editing `state_schema` or step output
|
`bind-output-to-state`, use it before hand-editing `state_schema` or step output
|
||||||
bindings.
|
bindings.
|
||||||
|
|||||||
@@ -18,11 +18,10 @@ validated, runnable deployment.
|
|||||||
for common edits.
|
for common edits.
|
||||||
- `set-input` and `set-output` replace full maps by default; pass `--merge`
|
- `set-input` and `set-output` replace full maps by default; pass `--merge`
|
||||||
only when adding or updating one entry across a later revision.
|
only when adding or updating one entry across a later revision.
|
||||||
- Before output-mapping into a new state field, declare it with
|
- Before output-mapping into a new state field, declare it with
|
||||||
`add-state-from-output` when it should mirror a capability output property.
|
`bind-output-to-state` when it should mirror a capability output property.
|
||||||
- Use `bind-output-to-state` when a capability output should become state;
|
It declares the matching state schema and merges the output binding in one
|
||||||
it declares the matching state schema and merges the output binding in one
|
revision-checked edit.
|
||||||
revision-checked edit.
|
|
||||||
- When adding a new capability-backed step, prefer:
|
- When adding a new capability-backed step, prefer:
|
||||||
```bash
|
```bash
|
||||||
wf draft add-step-from-capability ...
|
wf draft add-step-from-capability ...
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from wf_platform import CapabilitySource
|
|||||||
from .artifact_plans import raw_plan_from_artifact
|
from .artifact_plans import raw_plan_from_artifact
|
||||||
from .artifact_refs import artifact_capability_id
|
from .artifact_refs import artifact_capability_id
|
||||||
from .capability_requirements import required_capability_payloads
|
from .capability_requirements import required_capability_payloads
|
||||||
|
from .draft_authoring import WorkflowDraftAuthoringApi
|
||||||
from .drafts import WorkflowDraftApi
|
from .drafts import WorkflowDraftApi
|
||||||
from .listing import matches_query, paged_list_payload
|
from .listing import matches_query, paged_list_payload
|
||||||
from .next_actions import NextActions
|
from .next_actions import NextActions
|
||||||
@@ -64,6 +65,7 @@ class WorkflowCapabilityApi:
|
|||||||
def __init__(self, context: WorkflowOperationContext) -> None:
|
def __init__(self, context: WorkflowOperationContext) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
self.drafts = WorkflowDraftApi(context)
|
self.drafts = WorkflowDraftApi(context)
|
||||||
|
self.draft_authoring = WorkflowDraftAuthoringApi(context, self.drafts)
|
||||||
|
|
||||||
async def list_capabilities(
|
async def list_capabilities(
|
||||||
self,
|
self,
|
||||||
@@ -353,7 +355,7 @@ class WorkflowCapabilityApi:
|
|||||||
"""Create a patchable draft workspace from inspect_capability hints."""
|
"""Create a patchable draft workspace from inspect_capability hints."""
|
||||||
capability = await self.inspect_capability(qualified_name=capability_name)
|
capability = await self.inspect_capability(qualified_name=capability_name)
|
||||||
hints = capability["wrapper_hints"]
|
hints = capability["wrapper_hints"]
|
||||||
result = await self.drafts.create_minimal_draft_workspace(
|
result = await self.draft_authoring.create_minimal_draft_workspace(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
name=name or _draft_name_from_capability(capability_name),
|
name=name or _draft_name_from_capability(capability_name),
|
||||||
capability_name=capability_name,
|
capability_name=capability_name,
|
||||||
|
|||||||
@@ -0,0 +1,392 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from wf_core.models.steps import (
|
||||||
|
InputBinding,
|
||||||
|
OutputBinding,
|
||||||
|
)
|
||||||
|
from wf_core.paths import GraphSourcePath
|
||||||
|
|
||||||
|
from .constants import (
|
||||||
|
DEFAULT_CALL_STEP_ID,
|
||||||
|
DEFAULT_ERROR_OUTCOME,
|
||||||
|
DEFAULT_ERROR_STEP_ID,
|
||||||
|
DEFAULT_OK_OUTCOME,
|
||||||
|
RUNTIME_ERROR_CAPABILITY,
|
||||||
|
)
|
||||||
|
from .draft_payloads import (
|
||||||
|
_graph_path_payload,
|
||||||
|
draft_step,
|
||||||
|
escape_json_pointer,
|
||||||
|
input_bindings_payload,
|
||||||
|
output_bindings_payload,
|
||||||
|
state_root_field,
|
||||||
|
)
|
||||||
|
from .drafts import (
|
||||||
|
WorkflowDraftApi,
|
||||||
|
_draft_input_maps,
|
||||||
|
_draft_output_map,
|
||||||
|
)
|
||||||
|
from .operation_context import WorkflowOperationContext
|
||||||
|
from .schema_projection import project_output_property_to_state_schema
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowDraftAuthoringApi:
|
||||||
|
"""Capability-aware semantic edits over revisioned workflow drafts."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
context: WorkflowOperationContext,
|
||||||
|
drafts: WorkflowDraftApi,
|
||||||
|
) -> None:
|
||||||
|
self.context = context
|
||||||
|
self.drafts = drafts
|
||||||
|
|
||||||
|
def _outcomes_for_capability(self, qualified_name: str) -> tuple[str, ...] | None:
|
||||||
|
try:
|
||||||
|
spec = self.context.specs.get_qualified_spec(qualified_name)
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
outcomes = getattr(spec, "outcomes", None)
|
||||||
|
return tuple(outcomes) if outcomes is not None else None
|
||||||
|
|
||||||
|
async def create_minimal_draft_workspace(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
name: str,
|
||||||
|
capability_name: str,
|
||||||
|
input_schema: dict[str, Any],
|
||||||
|
state_schema: dict[str, Any],
|
||||||
|
output_schema: dict[str, Any],
|
||||||
|
input: Sequence[InputBinding] | None = None,
|
||||||
|
output: Sequence[OutputBinding] | None = None,
|
||||||
|
input_map: dict[str, str] | None = None,
|
||||||
|
output_map: dict[str, str] | None = None,
|
||||||
|
error_message_source: str | GraphSourcePath | None = None,
|
||||||
|
title: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Bootstrap the smallest patchable draft around one workflow capability."""
|
||||||
|
draft_input, draft_with = _draft_input_maps(
|
||||||
|
input=input,
|
||||||
|
input_map=input_map,
|
||||||
|
)
|
||||||
|
draft_output = _draft_output_map(output=output, output_map=output_map)
|
||||||
|
outcomes = self._outcomes_for_capability(capability_name) or (
|
||||||
|
DEFAULT_OK_OUTCOME,
|
||||||
|
)
|
||||||
|
steps: dict[str, Any] = {
|
||||||
|
DEFAULT_CALL_STEP_ID: {
|
||||||
|
"use": capability_name,
|
||||||
|
"input": input_bindings_payload(draft_input, draft_with),
|
||||||
|
"output": output_bindings_payload(draft_output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
routes: dict[str, dict[str, str]] = {
|
||||||
|
DEFAULT_CALL_STEP_ID: {DEFAULT_OK_OUTCOME: "__end__"}
|
||||||
|
}
|
||||||
|
if DEFAULT_ERROR_OUTCOME in outcomes:
|
||||||
|
error_input: dict[str, Any] = {
|
||||||
|
"target": "message",
|
||||||
|
"value": "Capability call failed",
|
||||||
|
}
|
||||||
|
if error_message_source is not None:
|
||||||
|
error_input = {
|
||||||
|
"target": "message",
|
||||||
|
"path": _graph_path_payload(error_message_source),
|
||||||
|
}
|
||||||
|
steps[DEFAULT_ERROR_STEP_ID] = {
|
||||||
|
"use": RUNTIME_ERROR_CAPABILITY,
|
||||||
|
"input": [error_input],
|
||||||
|
"output": [],
|
||||||
|
}
|
||||||
|
routes[DEFAULT_CALL_STEP_ID][DEFAULT_ERROR_OUTCOME] = DEFAULT_ERROR_STEP_ID
|
||||||
|
routes[DEFAULT_ERROR_STEP_ID] = {DEFAULT_OK_OUTCOME: "__end__"}
|
||||||
|
draft = {
|
||||||
|
"name": name,
|
||||||
|
"input_schema": input_schema,
|
||||||
|
"state_schema": state_schema,
|
||||||
|
"output_schema": output_schema,
|
||||||
|
"start": DEFAULT_CALL_STEP_ID,
|
||||||
|
"steps": steps,
|
||||||
|
"routes": routes,
|
||||||
|
}
|
||||||
|
return await self.drafts.create_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
title=title,
|
||||||
|
draft=draft,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def bind_output_to_state(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
output_field: str,
|
||||||
|
state_path: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Declare a state field from a step output and bind that output to it.
|
||||||
|
|
||||||
|
This is the common draft-authoring repair for validation errors where a
|
||||||
|
step writes to ``state.x`` before ``state_schema.properties.x`` exists.
|
||||||
|
It deliberately edits only one root state field and one step output map.
|
||||||
|
Route changes remain explicit through ``set_draft_route``.
|
||||||
|
"""
|
||||||
|
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||||
|
step = draft_step(workspace.draft, step_id)
|
||||||
|
capability_name = step.get("use")
|
||||||
|
if not isinstance(capability_name, str) or not capability_name:
|
||||||
|
raise ValueError(
|
||||||
|
f"draft step {step_id!r} does not declare a capability use"
|
||||||
|
)
|
||||||
|
|
||||||
|
state_field = state_root_field(state_path)
|
||||||
|
spec = self.context.specs.get_qualified_spec(capability_name)
|
||||||
|
output_schema = (
|
||||||
|
spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||||
|
)
|
||||||
|
state_schema = workspace.draft.get("state_schema", {})
|
||||||
|
if not isinstance(state_schema, dict):
|
||||||
|
raise ValueError("draft state_schema must be an object")
|
||||||
|
projected = project_output_property_to_state_schema(
|
||||||
|
state_schema=state_schema,
|
||||||
|
output_schema=output_schema,
|
||||||
|
output_field=output_field,
|
||||||
|
state_field=state_field,
|
||||||
|
)
|
||||||
|
output_map = {
|
||||||
|
**self.drafts._step_output_map(workspace_id=workspace_id, step_id=step_id),
|
||||||
|
output_field: state_path,
|
||||||
|
}
|
||||||
|
return await self.drafts.patch_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
patch=[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/state_schema",
|
||||||
|
"value": projected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": f"/steps/{escape_json_pointer(step_id)}/output",
|
||||||
|
"value": output_bindings_payload(output_map),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def add_step_from_capability(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
capability_name: str,
|
||||||
|
route_from_step: str | None = None,
|
||||||
|
route_from_outcome: str = DEFAULT_OK_OUTCOME,
|
||||||
|
routes: dict[str, str] | None = None,
|
||||||
|
input_map: dict[str, str] | None = None,
|
||||||
|
bind_outputs: dict[str, str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Add one capability step plus explicit route/map/schema wiring.
|
||||||
|
|
||||||
|
This is a composed authoring helper for agents. It edits the draft in
|
||||||
|
one revision so callers do not have to interleave add-step, route,
|
||||||
|
input-map, state-schema, and output-map operations by hand.
|
||||||
|
"""
|
||||||
|
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||||
|
steps = workspace.draft.get("steps")
|
||||||
|
if not isinstance(steps, dict):
|
||||||
|
raise ValueError("draft steps must be an object")
|
||||||
|
if step_id in steps:
|
||||||
|
raise ValueError(f"draft step {step_id!r} already exists")
|
||||||
|
|
||||||
|
spec = self.context.specs.get_qualified_spec(capability_name)
|
||||||
|
output_schema = (
|
||||||
|
spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||||
|
)
|
||||||
|
state_schema = workspace.draft.get("state_schema", {})
|
||||||
|
if not isinstance(state_schema, dict):
|
||||||
|
raise ValueError("draft state_schema must be an object")
|
||||||
|
|
||||||
|
declared_outcomes = self._outcomes_for_capability(capability_name)
|
||||||
|
if declared_outcomes is None:
|
||||||
|
declared_outcomes = (DEFAULT_OK_OUTCOME,)
|
||||||
|
|
||||||
|
if routes is not None:
|
||||||
|
missing_outcomes = set(declared_outcomes) - set(routes.keys())
|
||||||
|
unknown_outcomes = set(routes.keys()) - set(declared_outcomes)
|
||||||
|
if missing_outcomes or unknown_outcomes:
|
||||||
|
details = [
|
||||||
|
f"declared_outcomes={declared_outcomes!r}",
|
||||||
|
f"missing_outcomes={sorted(missing_outcomes)!r}",
|
||||||
|
f"unknown_outcomes={sorted(unknown_outcomes)!r}",
|
||||||
|
]
|
||||||
|
raise ValueError(
|
||||||
|
f"capability {capability_name!r} declares outcomes "
|
||||||
|
f"{declared_outcomes}, but routes has "
|
||||||
|
f"missing routes {sorted(missing_outcomes)} and unknown "
|
||||||
|
f"routes {sorted(unknown_outcomes)}; " + ", ".join(details)
|
||||||
|
)
|
||||||
|
step_routes = dict(routes)
|
||||||
|
else:
|
||||||
|
if len(declared_outcomes) == 1:
|
||||||
|
step_routes = {declared_outcomes[0]: "__end__"}
|
||||||
|
else:
|
||||||
|
missing_outcomes = sorted(declared_outcomes)
|
||||||
|
raise ValueError(
|
||||||
|
f"capability {capability_name!r} declares outcomes "
|
||||||
|
f"{declared_outcomes} with no routes supplied; missing "
|
||||||
|
f"routes for {missing_outcomes}"
|
||||||
|
)
|
||||||
|
|
||||||
|
input_map = input_map or {}
|
||||||
|
bind_outputs = bind_outputs or {}
|
||||||
|
projected_state_schema = state_schema
|
||||||
|
for output_field, path in bind_outputs.items():
|
||||||
|
sf = state_root_field(path)
|
||||||
|
projected_state_schema = project_output_property_to_state_schema(
|
||||||
|
state_schema=projected_state_schema,
|
||||||
|
output_schema=output_schema,
|
||||||
|
output_field=output_field,
|
||||||
|
state_field=sf,
|
||||||
|
)
|
||||||
|
|
||||||
|
patch: list[dict[str, Any]] = [
|
||||||
|
{
|
||||||
|
"op": "add",
|
||||||
|
"path": f"/steps/{escape_json_pointer(step_id)}",
|
||||||
|
"value": {
|
||||||
|
"use": capability_name,
|
||||||
|
"input": input_bindings_payload(input_map, {}),
|
||||||
|
"output": output_bindings_payload(bind_outputs),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "add",
|
||||||
|
"path": f"/routes/{escape_json_pointer(step_id)}",
|
||||||
|
"value": step_routes,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if projected_state_schema != state_schema:
|
||||||
|
patch.insert(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/state_schema",
|
||||||
|
"value": projected_state_schema,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if route_from_step is not None:
|
||||||
|
patch.append(
|
||||||
|
{
|
||||||
|
"op": "add",
|
||||||
|
"path": (
|
||||||
|
f"/routes/{escape_json_pointer(route_from_step)}/"
|
||||||
|
f"{escape_json_pointer(route_from_outcome)}"
|
||||||
|
),
|
||||||
|
"value": step_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.drafts.patch_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
patch=patch,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def branch_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
routes: dict[str, str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Atomically set routes for one step, preserving unspecified outcomes."""
|
||||||
|
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||||
|
draft_routes = workspace.draft.get("routes", {})
|
||||||
|
if not isinstance(draft_routes, dict):
|
||||||
|
raise ValueError("draft routes must be an object")
|
||||||
|
existing = draft_routes.get(step_id, {})
|
||||||
|
if not isinstance(existing, dict):
|
||||||
|
raise ValueError(f"routes for step {step_id!r} must be an object")
|
||||||
|
merged = {**existing, **routes}
|
||||||
|
if merged == existing:
|
||||||
|
return await self.drafts.get_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
)
|
||||||
|
return await self.drafts.patch_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
patch=[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": f"/routes/{escape_json_pointer(step_id)}",
|
||||||
|
"value": merged,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
branches: Sequence[DraftOutcomeRef],
|
||||||
|
target: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Update the target for multiple (step, outcome) pairs atomically."""
|
||||||
|
if not branches:
|
||||||
|
return await self.drafts.get_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
)
|
||||||
|
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||||
|
draft_routes = workspace.draft.get("routes", {})
|
||||||
|
if not isinstance(draft_routes, dict):
|
||||||
|
raise ValueError("draft routes must be an object")
|
||||||
|
patch: list[dict[str, Any]] = []
|
||||||
|
seen: set[tuple[str, str]] = set()
|
||||||
|
for ref in branches:
|
||||||
|
key = (ref.step_id, ref.outcome)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
step_routes = draft_routes.get(ref.step_id, {})
|
||||||
|
if not isinstance(step_routes, dict):
|
||||||
|
continue
|
||||||
|
if ref.outcome not in step_routes:
|
||||||
|
continue
|
||||||
|
if step_routes[ref.outcome] == target:
|
||||||
|
continue
|
||||||
|
patch.append(
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": (
|
||||||
|
f"/routes/{escape_json_pointer(ref.step_id)}/"
|
||||||
|
f"{escape_json_pointer(ref.outcome)}"
|
||||||
|
),
|
||||||
|
"value": target,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not patch:
|
||||||
|
return await self.drafts.get_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
)
|
||||||
|
return await self.drafts.patch_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
patch=patch,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DraftOutcomeRef:
|
||||||
|
"""A reference to a specific outcome of a draft step."""
|
||||||
|
|
||||||
|
step_id: str
|
||||||
|
outcome: str
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
|
||||||
|
|
||||||
|
|
||||||
|
def draft_step(draft: Mapping[str, Any], step_id: str) -> Mapping[str, Any]:
|
||||||
|
"""Return one step mapping from a draft, raising on missing or non-object."""
|
||||||
|
steps = draft.get("steps", {})
|
||||||
|
if not isinstance(steps, Mapping):
|
||||||
|
raise KeyError("draft steps are not available")
|
||||||
|
step = steps[step_id]
|
||||||
|
if not isinstance(step, Mapping):
|
||||||
|
raise KeyError(f"draft step {step_id!r} is not an object")
|
||||||
|
return step
|
||||||
|
|
||||||
|
|
||||||
|
def escape_json_pointer(value: str) -> str:
|
||||||
|
"""Escape one JSON Pointer path segment for generated JSON Patch helpers."""
|
||||||
|
return value.replace("~", "~0").replace("/", "~1")
|
||||||
|
|
||||||
|
|
||||||
|
def input_bindings_payload(
|
||||||
|
input_map: dict[str, str],
|
||||||
|
input_values: dict[str, Any],
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Serialize draft input maps into canonical string-path binding payloads."""
|
||||||
|
return [
|
||||||
|
{"target": _local_path_payload(target), "value": value}
|
||||||
|
for target, value in input_values.items()
|
||||||
|
] + [
|
||||||
|
{"target": _local_path_payload(target), "path": _graph_path_payload(source)}
|
||||||
|
for source, target in input_map.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def output_bindings_payload(output_map: dict[str, str]) -> list[dict[str, Any]]:
|
||||||
|
"""Serialize draft output maps into canonical string-path binding payloads."""
|
||||||
|
return [
|
||||||
|
{"source": _local_path_payload(source), "target": _state_path_payload(target)}
|
||||||
|
for source, target in output_map.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def state_root_field(value: str) -> str:
|
||||||
|
"""Return the single root field name from a state path, or raise."""
|
||||||
|
path = StatePath.parse(value)
|
||||||
|
if len(path.parts) != 1:
|
||||||
|
raise ValueError("state_path must name one root field, such as state.after")
|
||||||
|
return path.parts[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _local_path_payload(value: str) -> str:
|
||||||
|
return LocalPath._serialize(LocalPath.parse(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _graph_path_payload(value: str | GraphSourcePath) -> str:
|
||||||
|
path = value if isinstance(value, GraphSourcePath) else GraphSourcePath.parse(value)
|
||||||
|
return GraphSourcePath._serialize(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _state_path_payload(value: str) -> str:
|
||||||
|
return StatePath._serialize(StatePath.parse(value))
|
||||||
+19
-326
@@ -25,21 +25,24 @@ from wf_core.models.steps import (
|
|||||||
InputValueBinding,
|
InputValueBinding,
|
||||||
OutputBinding,
|
OutputBinding,
|
||||||
)
|
)
|
||||||
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
|
|
||||||
|
|
||||||
from .capability_requirements import (
|
from .capability_requirements import (
|
||||||
required_capabilities_for_plan,
|
required_capabilities_for_plan,
|
||||||
required_capability_payloads,
|
required_capability_payloads,
|
||||||
)
|
)
|
||||||
from .constants import (
|
from .draft_payloads import (
|
||||||
DEFAULT_CALL_STEP_ID,
|
draft_step as _draft_step,
|
||||||
DEFAULT_ERROR_OUTCOME,
|
)
|
||||||
DEFAULT_ERROR_STEP_ID,
|
from .draft_payloads import (
|
||||||
DEFAULT_OK_OUTCOME,
|
escape_json_pointer as _escape_json_pointer,
|
||||||
RUNTIME_ERROR_CAPABILITY,
|
)
|
||||||
|
from .draft_payloads import (
|
||||||
|
input_bindings_payload as _draft_input_bindings_payload,
|
||||||
|
)
|
||||||
|
from .draft_payloads import (
|
||||||
|
output_bindings_payload as _draft_output_bindings_payload,
|
||||||
)
|
)
|
||||||
from .operation_context import WorkflowOperationContext
|
from .operation_context import WorkflowOperationContext
|
||||||
from .schema_projection import project_output_property_to_state_schema
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowDraftApi:
|
class WorkflowDraftApi:
|
||||||
@@ -181,6 +184,14 @@ class WorkflowDraftApi:
|
|||||||
store.save_workspace(refreshed)
|
store.save_workspace(refreshed)
|
||||||
return get_draft_workspace_record(store, workspace_id=workspace_id)
|
return get_draft_workspace_record(store, workspace_id=workspace_id)
|
||||||
|
|
||||||
|
async def compile_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
|
||||||
|
"""Compile a stored draft workspace without mutating it."""
|
||||||
|
workspace = self._draft_store().get_workspace(workspace_id)
|
||||||
|
validation = await self.validate_draft(draft=workspace.draft)
|
||||||
|
if validation["status"] != "valid":
|
||||||
|
return validation
|
||||||
|
return await self.compile_draft(draft=workspace.draft)
|
||||||
|
|
||||||
async def patch_draft_workspace(
|
async def patch_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -288,197 +299,6 @@ class WorkflowDraftApi:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_state_schema_from_output(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
output_field: str,
|
|
||||||
state_path: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
workspace = self._draft_store().get_workspace(workspace_id)
|
|
||||||
step = _draft_step(workspace.draft, step_id)
|
|
||||||
capability_name = step.get("use")
|
|
||||||
if not isinstance(capability_name, str) or not capability_name:
|
|
||||||
raise ValueError(
|
|
||||||
f"draft step {step_id!r} does not declare a capability use"
|
|
||||||
)
|
|
||||||
state_field = _state_root_field(state_path)
|
|
||||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
|
||||||
output_schema = (
|
|
||||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
|
||||||
)
|
|
||||||
state_schema = workspace.draft.get("state_schema", {})
|
|
||||||
if not isinstance(state_schema, dict):
|
|
||||||
raise ValueError("draft state_schema must be an object")
|
|
||||||
projected = project_output_property_to_state_schema(
|
|
||||||
state_schema=state_schema,
|
|
||||||
output_schema=output_schema,
|
|
||||||
output_field=output_field,
|
|
||||||
state_field=state_field,
|
|
||||||
)
|
|
||||||
return await self.patch_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
patch=[
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/state_schema",
|
|
||||||
"value": projected,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def bind_output_to_state(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
output_field: str,
|
|
||||||
state_path: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Declare a state field from a step output and bind that output to it.
|
|
||||||
|
|
||||||
This is the common draft-authoring repair for validation errors where a
|
|
||||||
step writes to ``state.x`` before ``state_schema.properties.x`` exists.
|
|
||||||
It deliberately edits only one root state field and one step output map.
|
|
||||||
Route changes remain explicit through ``set_draft_route``.
|
|
||||||
"""
|
|
||||||
workspace = self._draft_store().get_workspace(workspace_id)
|
|
||||||
step = _draft_step(workspace.draft, step_id)
|
|
||||||
capability_name = step.get("use")
|
|
||||||
if not isinstance(capability_name, str) or not capability_name:
|
|
||||||
raise ValueError(
|
|
||||||
f"draft step {step_id!r} does not declare a capability use"
|
|
||||||
)
|
|
||||||
|
|
||||||
state_field = _state_root_field(state_path)
|
|
||||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
|
||||||
output_schema = (
|
|
||||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
|
||||||
)
|
|
||||||
state_schema = workspace.draft.get("state_schema", {})
|
|
||||||
if not isinstance(state_schema, dict):
|
|
||||||
raise ValueError("draft state_schema must be an object")
|
|
||||||
projected = project_output_property_to_state_schema(
|
|
||||||
state_schema=state_schema,
|
|
||||||
output_schema=output_schema,
|
|
||||||
output_field=output_field,
|
|
||||||
state_field=state_field,
|
|
||||||
)
|
|
||||||
output_map = {
|
|
||||||
**self._step_output_map(workspace_id=workspace_id, step_id=step_id),
|
|
||||||
output_field: state_path,
|
|
||||||
}
|
|
||||||
return await self.patch_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
patch=[
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/state_schema",
|
|
||||||
"value": projected,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": f"/steps/{_escape_json_pointer(step_id)}/output",
|
|
||||||
"value": _draft_output_bindings_payload(output_map),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def add_step_from_capability(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
capability_name: str,
|
|
||||||
route_from_step: str | None = None,
|
|
||||||
route_from_outcome: str = DEFAULT_OK_OUTCOME,
|
|
||||||
route_outcome: str = DEFAULT_OK_OUTCOME,
|
|
||||||
route_to: str = "__end__",
|
|
||||||
input_map: dict[str, str] | None = None,
|
|
||||||
bind_outputs: dict[str, str] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Add one capability step plus explicit route/map/schema wiring.
|
|
||||||
|
|
||||||
This is a composed authoring helper for agents. It edits the draft in
|
|
||||||
one revision so callers do not have to interleave add-step, route,
|
|
||||||
input-map, state-schema, and output-map operations by hand.
|
|
||||||
"""
|
|
||||||
workspace = self._draft_store().get_workspace(workspace_id)
|
|
||||||
steps = workspace.draft.get("steps")
|
|
||||||
if not isinstance(steps, dict):
|
|
||||||
raise ValueError("draft steps must be an object")
|
|
||||||
if step_id in steps:
|
|
||||||
raise ValueError(f"draft step {step_id!r} already exists")
|
|
||||||
|
|
||||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
|
||||||
output_schema = (
|
|
||||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
|
||||||
)
|
|
||||||
state_schema = workspace.draft.get("state_schema", {})
|
|
||||||
if not isinstance(state_schema, dict):
|
|
||||||
raise ValueError("draft state_schema must be an object")
|
|
||||||
|
|
||||||
input_map = input_map or {}
|
|
||||||
bind_outputs = bind_outputs or {}
|
|
||||||
projected_state_schema = state_schema
|
|
||||||
for output_field, state_path in bind_outputs.items():
|
|
||||||
state_field = _state_root_field(state_path)
|
|
||||||
projected_state_schema = project_output_property_to_state_schema(
|
|
||||||
state_schema=projected_state_schema,
|
|
||||||
output_schema=output_schema,
|
|
||||||
output_field=output_field,
|
|
||||||
state_field=state_field,
|
|
||||||
)
|
|
||||||
|
|
||||||
patch: list[dict[str, Any]] = [
|
|
||||||
{
|
|
||||||
"op": "add",
|
|
||||||
"path": f"/steps/{_escape_json_pointer(step_id)}",
|
|
||||||
"value": {
|
|
||||||
"use": capability_name,
|
|
||||||
"input": _draft_input_bindings_payload(input_map, {}),
|
|
||||||
"output": _draft_output_bindings_payload(bind_outputs),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "add",
|
|
||||||
"path": f"/routes/{_escape_json_pointer(step_id)}",
|
|
||||||
"value": {route_outcome: route_to},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if projected_state_schema != state_schema:
|
|
||||||
patch.insert(
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/state_schema",
|
|
||||||
"value": projected_state_schema,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if route_from_step is not None:
|
|
||||||
patch.append(
|
|
||||||
{
|
|
||||||
"op": "add",
|
|
||||||
"path": (
|
|
||||||
f"/routes/{_escape_json_pointer(route_from_step)}/"
|
|
||||||
f"{_escape_json_pointer(route_from_outcome)}"
|
|
||||||
),
|
|
||||||
"value": step_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return await self.patch_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
patch=patch,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _step_input_maps(
|
def _step_input_maps(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -494,76 +314,6 @@ class WorkflowDraftApi:
|
|||||||
step = _draft_step(workspace.draft, step_id)
|
step = _draft_step(workspace.draft, step_id)
|
||||||
return _output_map_from_payload(step.get("output", []))
|
return _output_map_from_payload(step.get("output", []))
|
||||||
|
|
||||||
async def create_minimal_draft_workspace(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
name: str,
|
|
||||||
capability_name: str,
|
|
||||||
input_schema: dict[str, Any],
|
|
||||||
state_schema: dict[str, Any],
|
|
||||||
output_schema: dict[str, Any],
|
|
||||||
input: Sequence[InputBinding] | None = None,
|
|
||||||
output: Sequence[OutputBinding] | None = None,
|
|
||||||
input_map: dict[str, str] | None = None,
|
|
||||||
output_map: dict[str, str] | None = None,
|
|
||||||
error_message_source: str | GraphSourcePath | None = None,
|
|
||||||
title: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Bootstrap the smallest patchable draft around one workflow capability."""
|
|
||||||
draft_input, draft_with = _draft_input_maps(
|
|
||||||
input=input,
|
|
||||||
input_map=input_map,
|
|
||||||
)
|
|
||||||
draft_output = _draft_output_map(output=output, output_map=output_map)
|
|
||||||
outcomes = self._outcomes_for_capability(capability_name) or (
|
|
||||||
DEFAULT_OK_OUTCOME,
|
|
||||||
)
|
|
||||||
steps: dict[str, Any] = {
|
|
||||||
DEFAULT_CALL_STEP_ID: {
|
|
||||||
"use": capability_name,
|
|
||||||
"input": _draft_input_bindings_payload(draft_input, draft_with),
|
|
||||||
"output": _draft_output_bindings_payload(draft_output),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
routes: dict[str, dict[str, str]] = {
|
|
||||||
DEFAULT_CALL_STEP_ID: {DEFAULT_OK_OUTCOME: "__end__"}
|
|
||||||
}
|
|
||||||
if DEFAULT_ERROR_OUTCOME in outcomes:
|
|
||||||
# The bootstrapper cannot infer provider-specific error envelopes.
|
|
||||||
# Use a static default unless the caller explicitly supplies the
|
|
||||||
# state path containing a better provider error message.
|
|
||||||
error_input: dict[str, Any] = {
|
|
||||||
"target": {"root": "local", "parts": ["message"]},
|
|
||||||
"value": "Capability call failed",
|
|
||||||
}
|
|
||||||
if error_message_source is not None:
|
|
||||||
error_input = {
|
|
||||||
"target": {"root": "local", "parts": ["message"]},
|
|
||||||
"path": _graph_path_payload(error_message_source),
|
|
||||||
}
|
|
||||||
steps[DEFAULT_ERROR_STEP_ID] = {
|
|
||||||
"use": RUNTIME_ERROR_CAPABILITY,
|
|
||||||
"input": [error_input],
|
|
||||||
"output": [],
|
|
||||||
}
|
|
||||||
routes[DEFAULT_CALL_STEP_ID][DEFAULT_ERROR_OUTCOME] = DEFAULT_ERROR_STEP_ID
|
|
||||||
routes[DEFAULT_ERROR_STEP_ID] = {DEFAULT_OK_OUTCOME: "__end__"}
|
|
||||||
draft = {
|
|
||||||
"name": name,
|
|
||||||
"input_schema": input_schema,
|
|
||||||
"state_schema": state_schema,
|
|
||||||
"output_schema": output_schema,
|
|
||||||
"start": DEFAULT_CALL_STEP_ID,
|
|
||||||
"steps": steps,
|
|
||||||
"routes": routes,
|
|
||||||
}
|
|
||||||
return await self.create_draft_workspace(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
title=title,
|
|
||||||
draft=draft,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _draft_input_maps(
|
def _draft_input_maps(
|
||||||
*,
|
*,
|
||||||
@@ -606,38 +356,6 @@ def _draft_output_map(
|
|||||||
return {str(binding.source): str(binding.target) for binding in output}
|
return {str(binding.source): str(binding.target) for binding in output}
|
||||||
|
|
||||||
|
|
||||||
def _draft_input_bindings_payload(
|
|
||||||
input_map: dict[str, str],
|
|
||||||
input_values: dict[str, Any],
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Serialize draft input maps into canonical structural binding payloads."""
|
|
||||||
return [
|
|
||||||
{"target": _local_path_payload(target), "value": value}
|
|
||||||
for target, value in input_values.items()
|
|
||||||
] + [
|
|
||||||
{"target": _local_path_payload(target), "path": _graph_path_payload(source)}
|
|
||||||
for source, target in input_map.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _draft_output_bindings_payload(output_map: dict[str, str]) -> list[dict[str, Any]]:
|
|
||||||
"""Serialize draft output maps into canonical structural binding payloads."""
|
|
||||||
return [
|
|
||||||
{"source": _local_path_payload(source), "target": _state_path_payload(target)}
|
|
||||||
for source, target in output_map.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _draft_step(draft: Mapping[str, Any], step_id: str) -> Mapping[str, Any]:
|
|
||||||
steps = draft.get("steps", {})
|
|
||||||
if not isinstance(steps, Mapping):
|
|
||||||
raise KeyError("draft steps are not available")
|
|
||||||
step = steps[step_id]
|
|
||||||
if not isinstance(step, Mapping):
|
|
||||||
raise KeyError(f"draft step {step_id!r} is not an object")
|
|
||||||
return step
|
|
||||||
|
|
||||||
|
|
||||||
def _input_maps_from_payload(
|
def _input_maps_from_payload(
|
||||||
payload: Any,
|
payload: Any,
|
||||||
) -> tuple[dict[str, str], dict[str, Any]]:
|
) -> tuple[dict[str, str], dict[str, Any]]:
|
||||||
@@ -694,24 +412,6 @@ def _path_text(value: Any, *, expected_root: str | None = None) -> str:
|
|||||||
return root if not raw_parts else f"{root}.{'.'.join(raw_parts)}"
|
return root if not raw_parts else f"{root}.{'.'.join(raw_parts)}"
|
||||||
|
|
||||||
|
|
||||||
def _graph_path_payload(value: str | GraphSourcePath) -> str:
|
|
||||||
path = value if isinstance(value, GraphSourcePath) else GraphSourcePath.parse(value)
|
|
||||||
return GraphSourcePath._serialize(path)
|
|
||||||
|
|
||||||
|
|
||||||
def _local_path_payload(value: str) -> str:
|
|
||||||
return LocalPath._serialize(LocalPath.parse(value))
|
|
||||||
|
|
||||||
|
|
||||||
def _state_path_payload(value: str) -> str:
|
|
||||||
return StatePath._serialize(StatePath.parse(value))
|
|
||||||
|
|
||||||
|
|
||||||
def _escape_json_pointer(value: str) -> str:
|
|
||||||
"""Escape one JSON Pointer path segment for generated JSON Patch helpers."""
|
|
||||||
return value.replace("~", "~0").replace("/", "~1")
|
|
||||||
|
|
||||||
|
|
||||||
def _with_workspace_repair_hints(
|
def _with_workspace_repair_hints(
|
||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
@@ -762,10 +462,3 @@ def _draft_repair_hint(
|
|||||||
f"wf draft bind-output-to-state {workspace_id} --revision {revision} "
|
f"wf draft bind-output-to-state {workspace_id} --revision {revision} "
|
||||||
f"--step {step_id} --output {output_field} --state {state_path}"
|
f"--step {step_id} --output {output_field} --state {state_path}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _state_root_field(value: str) -> str:
|
|
||||||
path = StatePath.parse(value)
|
|
||||||
if len(path.parts) != 1:
|
|
||||||
raise ValueError("state_path must name one root field, such as state.after")
|
|
||||||
return path.parts[0]
|
|
||||||
|
|||||||
+50
-24
@@ -8,6 +8,7 @@ from wf_artifacts import ArtifactKind
|
|||||||
from .artifacts import WorkflowArtifactApi
|
from .artifacts import WorkflowArtifactApi
|
||||||
from .capabilities import WorkflowCapabilityApi
|
from .capabilities import WorkflowCapabilityApi
|
||||||
from .deployments import WorkflowDeploymentApi
|
from .deployments import WorkflowDeploymentApi
|
||||||
|
from .draft_authoring import WorkflowDraftAuthoringApi
|
||||||
from .drafts import WorkflowDraftApi
|
from .drafts import WorkflowDraftApi
|
||||||
from .models import RawWorkflowPlan
|
from .models import RawWorkflowPlan
|
||||||
from .operation_context import WorkflowOperationContext
|
from .operation_context import WorkflowOperationContext
|
||||||
@@ -26,6 +27,7 @@ class WorkflowApi:
|
|||||||
self.context = context
|
self.context = context
|
||||||
self.capabilities = WorkflowCapabilityApi(context)
|
self.capabilities = WorkflowCapabilityApi(context)
|
||||||
self.drafts = WorkflowDraftApi(context)
|
self.drafts = WorkflowDraftApi(context)
|
||||||
|
self.draft_authoring = WorkflowDraftAuthoringApi(context, self.drafts)
|
||||||
self.artifacts = WorkflowArtifactApi(context)
|
self.artifacts = WorkflowArtifactApi(context)
|
||||||
self.deployments = WorkflowDeploymentApi(context)
|
self.deployments = WorkflowDeploymentApi(context)
|
||||||
self.runs = WorkflowRunApi(context)
|
self.runs = WorkflowRunApi(context)
|
||||||
@@ -285,6 +287,13 @@ class WorkflowApi:
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.drafts.validate_draft_workspace(workspace_id=workspace_id)
|
return await self.drafts.validate_draft_workspace(workspace_id=workspace_id)
|
||||||
|
|
||||||
|
async def compile_draft_workspace(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self.drafts.compile_draft_workspace(workspace_id=workspace_id)
|
||||||
|
|
||||||
async def patch_draft_workspace(
|
async def patch_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -362,23 +371,6 @@ class WorkflowApi:
|
|||||||
merge=merge,
|
merge=merge,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_state_schema_from_output(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
output_field: str,
|
|
||||||
state_path: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self.drafts.add_state_schema_from_output(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
step_id=step_id,
|
|
||||||
output_field=output_field,
|
|
||||||
state_path=state_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def bind_output_to_state(
|
async def bind_output_to_state(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -388,7 +380,7 @@ class WorkflowApi:
|
|||||||
output_field: str,
|
output_field: str,
|
||||||
state_path: str,
|
state_path: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.drafts.bind_output_to_state(
|
return await self.draft_authoring.bind_output_to_state(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
step_id=step_id,
|
step_id=step_id,
|
||||||
@@ -405,24 +397,58 @@ class WorkflowApi:
|
|||||||
capability_name: str,
|
capability_name: str,
|
||||||
route_from_step: str | None = None,
|
route_from_step: str | None = None,
|
||||||
route_from_outcome: str = "ok",
|
route_from_outcome: str = "ok",
|
||||||
route_outcome: str = "ok",
|
routes: dict[str, str] | None = None,
|
||||||
route_to: str = "__end__",
|
|
||||||
input_map: dict[str, str] | None = None,
|
input_map: dict[str, str] | None = None,
|
||||||
bind_outputs: dict[str, str] | None = None,
|
bind_outputs: dict[str, str] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.drafts.add_step_from_capability(
|
return await self.draft_authoring.add_step_from_capability(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
revision=revision,
|
revision=revision,
|
||||||
step_id=step_id,
|
step_id=step_id,
|
||||||
capability_name=capability_name,
|
capability_name=capability_name,
|
||||||
route_from_step=route_from_step,
|
route_from_step=route_from_step,
|
||||||
route_from_outcome=route_from_outcome,
|
route_from_outcome=route_from_outcome,
|
||||||
route_outcome=route_outcome,
|
routes=routes,
|
||||||
route_to=route_to,
|
|
||||||
input_map=input_map,
|
input_map=input_map,
|
||||||
bind_outputs=bind_outputs,
|
bind_outputs=bind_outputs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def branch_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
routes: dict[str, str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self.draft_authoring.branch_draft(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
step_id=step_id,
|
||||||
|
routes=routes,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
branches: list[dict[str, str]],
|
||||||
|
target: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
from .draft_authoring import DraftOutcomeRef
|
||||||
|
|
||||||
|
refs = [
|
||||||
|
DraftOutcomeRef(step_id=b["step_id"], outcome=b["outcome"])
|
||||||
|
for b in branches
|
||||||
|
]
|
||||||
|
return await self.draft_authoring.handle_draft(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
branches=refs,
|
||||||
|
target=target,
|
||||||
|
)
|
||||||
|
|
||||||
async def create_minimal_draft_workspace(
|
async def create_minimal_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -439,7 +465,7 @@ class WorkflowApi:
|
|||||||
error_message_source: Any | None = None,
|
error_message_source: Any | None = None,
|
||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.drafts.create_minimal_draft_workspace(
|
return await self.draft_authoring.create_minimal_draft_workspace(
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
name=name,
|
name=name,
|
||||||
capability_name=capability_name,
|
capability_name=capability_name,
|
||||||
|
|||||||
+25
-12
@@ -114,16 +114,6 @@ class WorkflowDraftSurface(Protocol):
|
|||||||
merge: bool = False,
|
merge: bool = False,
|
||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
async def add_state_schema_from_output(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
output_field: str,
|
|
||||||
state_path: str,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
async def bind_output_to_state(
|
async def bind_output_to_state(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -143,18 +133,41 @@ class WorkflowDraftSurface(Protocol):
|
|||||||
capability_name: str,
|
capability_name: str,
|
||||||
route_from_step: str | None = None,
|
route_from_step: str | None = None,
|
||||||
route_from_outcome: str = "ok",
|
route_from_outcome: str = "ok",
|
||||||
route_outcome: str = "ok",
|
routes: dict[str, str] | None = None,
|
||||||
route_to: str = "__end__",
|
|
||||||
input_map: dict[str, str] | None = None,
|
input_map: dict[str, str] | None = None,
|
||||||
bind_outputs: dict[str, str] | None = None,
|
bind_outputs: dict[str, str] | None = None,
|
||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def branch_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
routes: dict[str, str],
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def handle_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
branches: list[dict[str, str]],
|
||||||
|
target: str,
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
async def validate_draft_workspace(
|
async def validate_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
async def compile_draft_workspace(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
async def delete_draft_workspace(
|
async def delete_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
+116
-51
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated, Literal
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
@@ -277,47 +278,6 @@ def set_step_output_map(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command("add-state-from-output")
|
|
||||||
def add_state_from_output(
|
|
||||||
ctx: typer.Context,
|
|
||||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
|
||||||
revision: Annotated[
|
|
||||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
|
||||||
],
|
|
||||||
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
|
||||||
output_field: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Option("--output", help="Top-level capability output field."),
|
|
||||||
],
|
|
||||||
state_path: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Option("--state", help="Root state path, for example state.after."),
|
|
||||||
],
|
|
||||||
) -> None:
|
|
||||||
"""Copy one capability output field schema into draft state_schema.
|
|
||||||
|
|
||||||
Use this before mapping a step output into a new state field. The command
|
|
||||||
reads the selected draft step's capability output schema, copies the
|
|
||||||
requested output property schema, and preserves local $defs/definitions so
|
|
||||||
JSON Schema refs remain valid.
|
|
||||||
|
|
||||||
Run `wf draft validate <workspace_id>` after adding state schema fields.
|
|
||||||
"""
|
|
||||||
context = load_cli_context(ctx)
|
|
||||||
emit_json(
|
|
||||||
run_cli_operation(
|
|
||||||
context,
|
|
||||||
context.handlers.add_state_schema_from_output(
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
revision=revision,
|
|
||||||
step_id=step_id,
|
|
||||||
output_field=output_field,
|
|
||||||
state_path=state_path,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("bind-output-to-state")
|
@app.command("bind-output-to-state")
|
||||||
def bind_output_to_state(
|
def bind_output_to_state(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
@@ -381,14 +341,13 @@ def add_step_from_capability(
|
|||||||
str,
|
str,
|
||||||
typer.Option("--from-outcome", help="Outcome on --from-step."),
|
typer.Option("--from-outcome", help="Outcome on --from-step."),
|
||||||
] = "ok",
|
] = "ok",
|
||||||
route_outcome: Annotated[
|
route: Annotated[
|
||||||
str,
|
list[str] | None,
|
||||||
typer.Option("--outcome", help="Outcome emitted by the new step."),
|
typer.Option(
|
||||||
] = "ok",
|
"--route",
|
||||||
route_to: Annotated[
|
help="Route mapping OUTCOME=TARGET. Repeat for multiple outcomes.",
|
||||||
str,
|
),
|
||||||
typer.Option("--to", help="Target step id or __end__ for the new step."),
|
] = None,
|
||||||
] = "__end__",
|
|
||||||
input_mapping: Annotated[
|
input_mapping: Annotated[
|
||||||
list[str] | None,
|
list[str] | None,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
@@ -414,6 +373,15 @@ def add_step_from_capability(
|
|||||||
"""
|
"""
|
||||||
input_map = _parse_map_flags(input_mapping)
|
input_map = _parse_map_flags(input_mapping)
|
||||||
bind_outputs = _parse_map_flags(output_mapping)
|
bind_outputs = _parse_map_flags(output_mapping)
|
||||||
|
routes: dict[str, str] = {}
|
||||||
|
if route:
|
||||||
|
for r in route:
|
||||||
|
key, _, value = r.partition("=")
|
||||||
|
if not key or not value:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
f"invalid route: {r!r} (expected OUTCOME=TARGET)"
|
||||||
|
)
|
||||||
|
routes[key] = value
|
||||||
context = load_cli_context(ctx)
|
context = load_cli_context(ctx)
|
||||||
emit_json(
|
emit_json(
|
||||||
run_cli_operation(
|
run_cli_operation(
|
||||||
@@ -425,8 +393,7 @@ def add_step_from_capability(
|
|||||||
capability_name=capability_name,
|
capability_name=capability_name,
|
||||||
route_from_step=route_from_step,
|
route_from_step=route_from_step,
|
||||||
route_from_outcome=route_from_outcome,
|
route_from_outcome=route_from_outcome,
|
||||||
route_outcome=route_outcome,
|
routes=routes or None,
|
||||||
route_to=route_to,
|
|
||||||
input_map=input_map,
|
input_map=input_map,
|
||||||
bind_outputs=bind_outputs,
|
bind_outputs=bind_outputs,
|
||||||
),
|
),
|
||||||
@@ -434,6 +401,86 @@ def add_step_from_capability(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("branch")
|
||||||
|
def branch_draft(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||||
|
revision: Annotated[
|
||||||
|
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||||
|
],
|
||||||
|
step: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||||
|
route: Annotated[
|
||||||
|
list[str] | None,
|
||||||
|
typer.Option(
|
||||||
|
"--route",
|
||||||
|
help="Route mapping OUTCOME=TARGET. Repeat for multiple outcomes.",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Branch multiple outcome routes on a single step atomically."""
|
||||||
|
routes: dict[str, str] = {}
|
||||||
|
if route:
|
||||||
|
for r in route:
|
||||||
|
key, _, value = r.partition("=")
|
||||||
|
if not key or not value:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
f"invalid route: {r!r} (expected OUTCOME=TARGET)"
|
||||||
|
)
|
||||||
|
routes[key] = value
|
||||||
|
context = load_cli_context(ctx)
|
||||||
|
emit_json(
|
||||||
|
run_cli_operation(
|
||||||
|
context,
|
||||||
|
context.handlers.branch_draft(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
step_id=step,
|
||||||
|
routes=routes,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("handle")
|
||||||
|
def handle_draft(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||||
|
revision: Annotated[
|
||||||
|
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||||
|
],
|
||||||
|
to: Annotated[str, typer.Option("--to", help="Target step id or __end__.")],
|
||||||
|
branch: Annotated[
|
||||||
|
list[str] | None,
|
||||||
|
typer.Option(
|
||||||
|
"--branch",
|
||||||
|
help="Branch mapping STEP:OUTCOME. Repeat for multiple branches.",
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set a common target for multiple step/outcome pairs atomically."""
|
||||||
|
branches: list[dict[str, str]] = []
|
||||||
|
if branch:
|
||||||
|
for b in branch:
|
||||||
|
parts = b.rsplit(":", 1)
|
||||||
|
if len(parts) != 2 or not parts[0] or not parts[1]:
|
||||||
|
raise typer.BadParameter(
|
||||||
|
f"invalid branch: {b!r} (expected STEP:OUTCOME)"
|
||||||
|
)
|
||||||
|
branches.append({"step_id": parts[0], "outcome": parts[1]})
|
||||||
|
context = load_cli_context(ctx)
|
||||||
|
emit_json(
|
||||||
|
run_cli_operation(
|
||||||
|
context,
|
||||||
|
context.handlers.handle_draft(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
branches=branches,
|
||||||
|
target=to,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command("validate")
|
@app.command("validate")
|
||||||
def validate_draft(
|
def validate_draft(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
@@ -449,6 +496,24 @@ def validate_draft(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("compile")
|
||||||
|
def compile_draft(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||||
|
) -> None:
|
||||||
|
"""Compile a stored draft workspace without mutating it."""
|
||||||
|
context = load_cli_context(ctx)
|
||||||
|
result = run_cli_operation(
|
||||||
|
context,
|
||||||
|
context.handlers.compile_draft_workspace(workspace_id=workspace_id),
|
||||||
|
)
|
||||||
|
if "compiled_plan" in result:
|
||||||
|
emit_json(result["compiled_plan"])
|
||||||
|
return
|
||||||
|
typer.echo(json.dumps(result, indent=2, sort_keys=True), err=True)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
@app.command("delete")
|
@app.command("delete")
|
||||||
def delete_draft(
|
def delete_draft(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
|
|||||||
@@ -187,6 +187,12 @@ class ValidateDraftWorkspaceRequest(BaseModel):
|
|||||||
workspace_id: WorkspaceId
|
workspace_id: WorkspaceId
|
||||||
|
|
||||||
|
|
||||||
|
class CompileDraftWorkspaceRequest(BaseModel):
|
||||||
|
"""Typed MCP request for compiling one workspace draft without mutation."""
|
||||||
|
|
||||||
|
workspace_id: WorkspaceId
|
||||||
|
|
||||||
|
|
||||||
class SetDraftNameRequest(BaseModel):
|
class SetDraftNameRequest(BaseModel):
|
||||||
"""Typed MCP request for changing the workflow draft name."""
|
"""Typed MCP request for changing the workflow draft name."""
|
||||||
|
|
||||||
@@ -237,20 +243,6 @@ class SetStepOutputMapRequest(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AddStateFromOutputRequest(BaseModel):
|
|
||||||
"""Typed MCP request for declaring a state field from a step output schema."""
|
|
||||||
|
|
||||||
workspace_id: WorkspaceId
|
|
||||||
revision: int = Field(ge=1, description="Expected current workspace revision.")
|
|
||||||
step_id: str = Field(description="Draft step id whose capability output is used.")
|
|
||||||
output_field: str = Field(
|
|
||||||
description="Top-level output field to copy, for example after."
|
|
||||||
)
|
|
||||||
state_path: str = Field(
|
|
||||||
description="Root state path to declare, for example state.after."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BindOutputToStateRequest(BaseModel):
|
class BindOutputToStateRequest(BaseModel):
|
||||||
"""Typed MCP request for binding one step output to one root state field."""
|
"""Typed MCP request for binding one step output to one root state field."""
|
||||||
|
|
||||||
@@ -280,13 +272,13 @@ class AddStepFromCapabilityRequest(BaseModel):
|
|||||||
default="ok",
|
default="ok",
|
||||||
description="Outcome on route_from_step that should route to the new step.",
|
description="Outcome on route_from_step that should route to the new step.",
|
||||||
)
|
)
|
||||||
route_outcome: str = Field(
|
routes: dict[str, str] | None = Field(
|
||||||
default="ok",
|
default=None,
|
||||||
description="Outcome emitted by the new step.",
|
description=(
|
||||||
)
|
"Outcome to target route map for the new step. If omitted, a single "
|
||||||
route_to: str = Field(
|
"declared outcome routes to __end__; multiple declared outcomes "
|
||||||
default="__end__",
|
"require explicit routes."
|
||||||
description="Target step id or __end__ for the new step outcome.",
|
),
|
||||||
)
|
)
|
||||||
input_map: dict[str, str] = Field(
|
input_map: dict[str, str] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
@@ -298,6 +290,31 @@ class AddStepFromCapabilityRequest(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BranchDraftRequest(BaseModel):
|
||||||
|
"""Typed MCP request for branching routes on a draft step."""
|
||||||
|
|
||||||
|
workspace_id: WorkspaceId
|
||||||
|
revision: int = Field(ge=1, description="Expected workspace revision.")
|
||||||
|
step_id: str = Field(description="Draft step id whose routes should be branched.")
|
||||||
|
routes: dict[str, str] = Field(description="Outcome to target step map.")
|
||||||
|
|
||||||
|
|
||||||
|
class HandleDraftBranchItem(BaseModel):
|
||||||
|
"""Single branch item for handle_draft."""
|
||||||
|
|
||||||
|
step_id: str = Field(description="Draft step id.")
|
||||||
|
outcome: str = Field(description="Outcome label.")
|
||||||
|
|
||||||
|
|
||||||
|
class HandleDraftRequest(BaseModel):
|
||||||
|
"""Typed MCP request for handling draft branches."""
|
||||||
|
|
||||||
|
workspace_id: WorkspaceId
|
||||||
|
revision: int = Field(ge=1, description="Expected workspace revision.")
|
||||||
|
branches: list[HandleDraftBranchItem] = Field(description="Branch items to handle.")
|
||||||
|
target: str = Field(description="Target step id or __end__.")
|
||||||
|
|
||||||
|
|
||||||
class DeleteDraftWorkspaceRequest(BaseModel):
|
class DeleteDraftWorkspaceRequest(BaseModel):
|
||||||
"""Typed MCP request payload for deleting one draft workspace."""
|
"""Typed MCP request payload for deleting one draft workspace."""
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ from wf_mcp.broker.service import WfMcpService
|
|||||||
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
AddStateFromOutputRequest,
|
|
||||||
AddStepFromCapabilityRequest,
|
AddStepFromCapabilityRequest,
|
||||||
BindOutputToStateRequest,
|
BindOutputToStateRequest,
|
||||||
|
BranchDraftRequest,
|
||||||
CallCapabilityResult,
|
CallCapabilityResult,
|
||||||
|
CompileDraftWorkspaceRequest,
|
||||||
CreateArtifactFromWorkspaceRequest,
|
CreateArtifactFromWorkspaceRequest,
|
||||||
CreateDraftWorkspaceFromCapabilityRequest,
|
CreateDraftWorkspaceFromCapabilityRequest,
|
||||||
CreateDraftWorkspaceFromCapabilityResult,
|
CreateDraftWorkspaceFromCapabilityResult,
|
||||||
@@ -26,6 +27,7 @@ from .models import (
|
|||||||
DeleteDraftWorkspaceResult,
|
DeleteDraftWorkspaceResult,
|
||||||
DraftWorkspaceListResult,
|
DraftWorkspaceListResult,
|
||||||
DraftWorkspaceResult,
|
DraftWorkspaceResult,
|
||||||
|
HandleDraftRequest,
|
||||||
PatchDraftWorkspaceRequest,
|
PatchDraftWorkspaceRequest,
|
||||||
RunDeploymentResult,
|
RunDeploymentResult,
|
||||||
SetDraftNameRequest,
|
SetDraftNameRequest,
|
||||||
@@ -368,6 +370,22 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
await handlers.validate_draft_workspace(workspace_id=request.workspace_id)
|
await handlers.validate_draft_workspace(workspace_id=request.workspace_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.compile_draft_workspace",
|
||||||
|
title="Compile Draft Workspace",
|
||||||
|
description=(
|
||||||
|
"Compile a stored draft workspace without mutating it. Returns "
|
||||||
|
"compiled_plan and required_capabilities when valid, or diagnostics "
|
||||||
|
"when invalid."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def compile_draft_workspace(
|
||||||
|
request: CompileDraftWorkspaceRequest,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await handlers.compile_draft_workspace(
|
||||||
|
workspace_id=request.workspace_id,
|
||||||
|
)
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.set_draft_name",
|
name="wf.workflow.set_draft_name",
|
||||||
title="Set Draft Name",
|
title="Set Draft Name",
|
||||||
@@ -442,27 +460,6 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@server.tool(
|
|
||||||
name="wf.workflow.add_state_from_output",
|
|
||||||
title="Add State From Output",
|
|
||||||
description=(
|
|
||||||
"Declare one root state field by copying a draft step capability output "
|
|
||||||
"field schema, including local $defs/definitions when present."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
async def add_state_from_output(
|
|
||||||
request: AddStateFromOutputRequest,
|
|
||||||
) -> DraftWorkspaceResult:
|
|
||||||
return DraftWorkspaceResult.model_validate(
|
|
||||||
await handlers.add_state_schema_from_output(
|
|
||||||
workspace_id=request.workspace_id,
|
|
||||||
revision=request.revision,
|
|
||||||
step_id=request.step_id,
|
|
||||||
output_field=request.output_field,
|
|
||||||
state_path=request.state_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.bind_output_to_state",
|
name="wf.workflow.bind_output_to_state",
|
||||||
title="Bind Output To State",
|
title="Bind Output To State",
|
||||||
@@ -503,13 +500,48 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
capability_name=request.capability_name,
|
capability_name=request.capability_name,
|
||||||
route_from_step=request.route_from_step,
|
route_from_step=request.route_from_step,
|
||||||
route_from_outcome=request.route_from_outcome,
|
route_from_outcome=request.route_from_outcome,
|
||||||
route_outcome=request.route_outcome,
|
routes=request.routes,
|
||||||
route_to=request.route_to,
|
|
||||||
input_map=request.input_map,
|
input_map=request.input_map,
|
||||||
bind_outputs=request.bind_outputs,
|
bind_outputs=request.bind_outputs,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.branch_draft",
|
||||||
|
title="Branch Draft",
|
||||||
|
description=(
|
||||||
|
"Branch multiple outcome routes on a single draft step atomically. "
|
||||||
|
"Provided outcomes are updated while unspecified outcomes are preserved."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def branch_draft(request: BranchDraftRequest) -> DraftWorkspaceResult:
|
||||||
|
return DraftWorkspaceResult.model_validate(
|
||||||
|
await handlers.branch_draft(
|
||||||
|
workspace_id=request.workspace_id,
|
||||||
|
revision=request.revision,
|
||||||
|
step_id=request.step_id,
|
||||||
|
routes=request.routes,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.handle_draft",
|
||||||
|
title="Handle Draft",
|
||||||
|
description=("Set a common target for multiple step/outcome pairs atomically."),
|
||||||
|
)
|
||||||
|
async def handle_draft(request: HandleDraftRequest) -> DraftWorkspaceResult:
|
||||||
|
return DraftWorkspaceResult.model_validate(
|
||||||
|
await handlers.handle_draft(
|
||||||
|
workspace_id=request.workspace_id,
|
||||||
|
revision=request.revision,
|
||||||
|
branches=[
|
||||||
|
{"step_id": b.step_id, "outcome": b.outcome}
|
||||||
|
for b in request.branches
|
||||||
|
],
|
||||||
|
target=request.target,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.create_minimal_draft_workspace",
|
name="wf.workflow.create_minimal_draft_workspace",
|
||||||
title="Create Minimal Draft Workspace",
|
title="Create Minimal Draft Workspace",
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ from .errors import WorkflowRpcError
|
|||||||
from .models import (
|
from .models import (
|
||||||
AddStepFromCapabilityParams,
|
AddStepFromCapabilityParams,
|
||||||
AdminEmptyParams,
|
AdminEmptyParams,
|
||||||
|
BranchDraftParams,
|
||||||
CallCapabilityParams,
|
CallCapabilityParams,
|
||||||
|
CompileDraftWorkspaceParams,
|
||||||
CreateArtifactFromPlanParams,
|
CreateArtifactFromPlanParams,
|
||||||
CreateArtifactFromWorkspaceParams,
|
CreateArtifactFromWorkspaceParams,
|
||||||
CreateDraftFromCapabilityParams,
|
CreateDraftFromCapabilityParams,
|
||||||
CreateWrapperFromWorkspaceParams,
|
CreateWrapperFromWorkspaceParams,
|
||||||
DeleteDeploymentParams,
|
DeleteDeploymentParams,
|
||||||
GetDraftWorkspaceParams,
|
GetDraftWorkspaceParams,
|
||||||
|
HandleDraftBranch,
|
||||||
|
HandleDraftParams,
|
||||||
HealthParams,
|
HealthParams,
|
||||||
InspectArtifactParams,
|
InspectArtifactParams,
|
||||||
InspectCapabilityParams,
|
InspectCapabilityParams,
|
||||||
@@ -42,15 +46,19 @@ from .models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CreateArtifactFromPlanParams",
|
|
||||||
"CreateArtifactFromWorkspaceParams",
|
|
||||||
"AddStepFromCapabilityParams",
|
"AddStepFromCapabilityParams",
|
||||||
"AdminEmptyParams",
|
"AdminEmptyParams",
|
||||||
|
"BranchDraftParams",
|
||||||
"CallCapabilityParams",
|
"CallCapabilityParams",
|
||||||
|
"CompileDraftWorkspaceParams",
|
||||||
|
"CreateArtifactFromPlanParams",
|
||||||
|
"CreateArtifactFromWorkspaceParams",
|
||||||
"CreateDraftFromCapabilityParams",
|
"CreateDraftFromCapabilityParams",
|
||||||
"CreateWrapperFromWorkspaceParams",
|
"CreateWrapperFromWorkspaceParams",
|
||||||
"DeleteDeploymentParams",
|
"DeleteDeploymentParams",
|
||||||
"GetDraftWorkspaceParams",
|
"GetDraftWorkspaceParams",
|
||||||
|
"HandleDraftBranch",
|
||||||
|
"HandleDraftParams",
|
||||||
"HealthParams",
|
"HealthParams",
|
||||||
"InspectArtifactParams",
|
"InspectArtifactParams",
|
||||||
"InspectCapabilityParams",
|
"InspectCapabilityParams",
|
||||||
|
|||||||
@@ -141,26 +141,6 @@ class RpcDraftClientMixin:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_state_schema_from_output(
|
|
||||||
self: RpcCaller,
|
|
||||||
*,
|
|
||||||
workspace_id: str,
|
|
||||||
revision: int,
|
|
||||||
step_id: str,
|
|
||||||
output_field: str,
|
|
||||||
state_path: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await self._call(
|
|
||||||
"workflow.draft_workspaces.add_state_from_output",
|
|
||||||
{
|
|
||||||
"workspace_id": workspace_id,
|
|
||||||
"revision": revision,
|
|
||||||
"step_id": step_id,
|
|
||||||
"output_field": output_field,
|
|
||||||
"state_path": state_path,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def bind_output_to_state(
|
async def bind_output_to_state(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
@@ -190,8 +170,7 @@ class RpcDraftClientMixin:
|
|||||||
capability_name: str,
|
capability_name: str,
|
||||||
route_from_step: str | None = None,
|
route_from_step: str | None = None,
|
||||||
route_from_outcome: str = "ok",
|
route_from_outcome: str = "ok",
|
||||||
route_outcome: str = "ok",
|
routes: dict[str, str] | None = None,
|
||||||
route_to: str = "__end__",
|
|
||||||
input_map: dict[str, str] | None = None,
|
input_map: dict[str, str] | None = None,
|
||||||
bind_outputs: dict[str, str] | None = None,
|
bind_outputs: dict[str, str] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -204,13 +183,48 @@ class RpcDraftClientMixin:
|
|||||||
"capability_name": capability_name,
|
"capability_name": capability_name,
|
||||||
"route_from_step": route_from_step,
|
"route_from_step": route_from_step,
|
||||||
"route_from_outcome": route_from_outcome,
|
"route_from_outcome": route_from_outcome,
|
||||||
"route_outcome": route_outcome,
|
"routes": routes,
|
||||||
"route_to": route_to,
|
|
||||||
"input_map": input_map or {},
|
"input_map": input_map or {},
|
||||||
"bind_outputs": bind_outputs or {},
|
"bind_outputs": bind_outputs or {},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def branch_draft(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
step_id: str,
|
||||||
|
routes: dict[str, str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.branch",
|
||||||
|
{
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"step_id": step_id,
|
||||||
|
"routes": routes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_draft(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
branches: list[dict[str, str]],
|
||||||
|
target: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.handle",
|
||||||
|
{
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"branches": branches,
|
||||||
|
"target": target,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def validate_draft_workspace(
|
async def validate_draft_workspace(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
@@ -221,6 +235,16 @@ class RpcDraftClientMixin:
|
|||||||
{"workspace_id": workspace_id},
|
{"workspace_id": workspace_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def compile_draft_workspace(
|
||||||
|
self: RpcCaller,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.compile",
|
||||||
|
{"workspace_id": workspace_id},
|
||||||
|
)
|
||||||
|
|
||||||
async def delete_draft_workspace(
|
async def delete_draft_workspace(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ from wf_server import WorkflowServer
|
|||||||
|
|
||||||
from ..errors import WorkflowRpcError, raise_workflow_rpc_error
|
from ..errors import WorkflowRpcError, raise_workflow_rpc_error
|
||||||
from ..models import (
|
from ..models import (
|
||||||
AddStateFromOutputParams,
|
|
||||||
AddStepFromCapabilityParams,
|
AddStepFromCapabilityParams,
|
||||||
BindOutputToStateParams,
|
BindOutputToStateParams,
|
||||||
|
BranchDraftParams,
|
||||||
|
CompileDraftWorkspaceParams,
|
||||||
CreateArtifactFromWorkspaceParams,
|
CreateArtifactFromWorkspaceParams,
|
||||||
CreateDraftFromCapabilityParams,
|
CreateDraftFromCapabilityParams,
|
||||||
CreateWrapperFromWorkspaceParams,
|
CreateWrapperFromWorkspaceParams,
|
||||||
DeleteDraftWorkspaceParams,
|
DeleteDraftWorkspaceParams,
|
||||||
GetDraftWorkspaceParams,
|
GetDraftWorkspaceParams,
|
||||||
|
HandleDraftParams,
|
||||||
ListDraftWorkspacesParams,
|
ListDraftWorkspacesParams,
|
||||||
PatchDraftParams,
|
PatchDraftParams,
|
||||||
PatchDraftWorkspaceParams,
|
PatchDraftWorkspaceParams,
|
||||||
@@ -180,24 +182,6 @@ def register_methods(
|
|||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
raise_workflow_rpc_error(exc)
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
@entrypoint.method(
|
|
||||||
name="workflow.draft_workspaces.add_state_from_output",
|
|
||||||
errors=[WorkflowRpcError],
|
|
||||||
)
|
|
||||||
async def workflow_draft_workspaces_add_state_from_output(
|
|
||||||
params: AddStateFromOutputParams = RpcParams(),
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
try:
|
|
||||||
return await server.api.add_state_schema_from_output(
|
|
||||||
workspace_id=params.workspace_id,
|
|
||||||
revision=params.revision,
|
|
||||||
step_id=params.step_id,
|
|
||||||
output_field=params.output_field,
|
|
||||||
state_path=params.state_path,
|
|
||||||
)
|
|
||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
|
||||||
raise_workflow_rpc_error(exc)
|
|
||||||
|
|
||||||
@entrypoint.method(
|
@entrypoint.method(
|
||||||
name="workflow.draft_workspaces.bind_output_to_state",
|
name="workflow.draft_workspaces.bind_output_to_state",
|
||||||
errors=[WorkflowRpcError],
|
errors=[WorkflowRpcError],
|
||||||
@@ -231,14 +215,48 @@ def register_methods(
|
|||||||
capability_name=params.capability_name,
|
capability_name=params.capability_name,
|
||||||
route_from_step=params.route_from_step,
|
route_from_step=params.route_from_step,
|
||||||
route_from_outcome=params.route_from_outcome,
|
route_from_outcome=params.route_from_outcome,
|
||||||
route_outcome=params.route_outcome,
|
routes=params.routes,
|
||||||
route_to=params.route_to,
|
|
||||||
input_map=params.input_map,
|
input_map=params.input_map,
|
||||||
bind_outputs=params.bind_outputs,
|
bind_outputs=params.bind_outputs,
|
||||||
)
|
)
|
||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
raise_workflow_rpc_error(exc)
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
|
@entrypoint.method(
|
||||||
|
name="workflow.draft_workspaces.branch", errors=[WorkflowRpcError]
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_branch(
|
||||||
|
params: BranchDraftParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.branch_draft(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
revision=params.revision,
|
||||||
|
step_id=params.step_id,
|
||||||
|
routes=params.routes,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
|
@entrypoint.method(
|
||||||
|
name="workflow.draft_workspaces.handle", errors=[WorkflowRpcError]
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_handle(
|
||||||
|
params: HandleDraftParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.handle_draft(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
revision=params.revision,
|
||||||
|
branches=[
|
||||||
|
{"step_id": b.step_id, "outcome": b.outcome}
|
||||||
|
for b in params.branches
|
||||||
|
],
|
||||||
|
target=params.target,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
@entrypoint.method(
|
@entrypoint.method(
|
||||||
name="workflow.draft_workspaces.validate", errors=[WorkflowRpcError]
|
name="workflow.draft_workspaces.validate", errors=[WorkflowRpcError]
|
||||||
)
|
)
|
||||||
@@ -252,6 +270,19 @@ def register_methods(
|
|||||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
raise_workflow_rpc_error(exc)
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
|
@entrypoint.method(
|
||||||
|
name="workflow.draft_workspaces.compile", errors=[WorkflowRpcError]
|
||||||
|
)
|
||||||
|
async def workflow_draft_workspaces_compile(
|
||||||
|
params: CompileDraftWorkspaceParams = RpcParams(),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await server.api.compile_draft_workspace(
|
||||||
|
workspace_id=params.workspace_id,
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||||
|
raise_workflow_rpc_error(exc)
|
||||||
|
|
||||||
@entrypoint.method(
|
@entrypoint.method(
|
||||||
name="workflow.draft_workspaces.delete", errors=[WorkflowRpcError]
|
name="workflow.draft_workspaces.delete", errors=[WorkflowRpcError]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -141,14 +141,6 @@ class SetStepOutputMapParams(RpcParamsModel):
|
|||||||
merge: bool = False
|
merge: bool = False
|
||||||
|
|
||||||
|
|
||||||
class AddStateFromOutputParams(RpcParamsModel):
|
|
||||||
workspace_id: str = Field(min_length=1)
|
|
||||||
revision: int = Field(ge=1)
|
|
||||||
step_id: str = Field(min_length=1)
|
|
||||||
output_field: str = Field(min_length=1)
|
|
||||||
state_path: str = Field(min_length=1)
|
|
||||||
|
|
||||||
|
|
||||||
class BindOutputToStateParams(RpcParamsModel):
|
class BindOutputToStateParams(RpcParamsModel):
|
||||||
workspace_id: str = Field(min_length=1)
|
workspace_id: str = Field(min_length=1)
|
||||||
revision: int = Field(ge=1)
|
revision: int = Field(ge=1)
|
||||||
@@ -164,16 +156,38 @@ class AddStepFromCapabilityParams(RpcParamsModel):
|
|||||||
capability_name: str = Field(min_length=1)
|
capability_name: str = Field(min_length=1)
|
||||||
route_from_step: str | None = None
|
route_from_step: str | None = None
|
||||||
route_from_outcome: str = Field(default="ok", min_length=1)
|
route_from_outcome: str = Field(default="ok", min_length=1)
|
||||||
route_outcome: str = Field(default="ok", min_length=1)
|
routes: dict[str, str] | None = None
|
||||||
route_to: str = Field(default="__end__", min_length=1)
|
|
||||||
input_map: dict[str, str] = Field(default_factory=dict)
|
input_map: dict[str, str] = Field(default_factory=dict)
|
||||||
bind_outputs: dict[str, str] = Field(default_factory=dict)
|
bind_outputs: dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class BranchDraftParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
revision: int = Field(ge=1)
|
||||||
|
step_id: str = Field(min_length=1)
|
||||||
|
routes: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
class HandleDraftBranch(RpcParamsModel):
|
||||||
|
step_id: str = Field(min_length=1)
|
||||||
|
outcome: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class HandleDraftParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
revision: int = Field(ge=1)
|
||||||
|
branches: list[HandleDraftBranch]
|
||||||
|
target: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
class ValidateDraftWorkspaceParams(RpcParamsModel):
|
class ValidateDraftWorkspaceParams(RpcParamsModel):
|
||||||
workspace_id: str = Field(min_length=1)
|
workspace_id: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class CompileDraftWorkspaceParams(RpcParamsModel):
|
||||||
|
workspace_id: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
class DeleteDraftWorkspaceParams(RpcParamsModel):
|
class DeleteDraftWorkspaceParams(RpcParamsModel):
|
||||||
workspace_id: str = Field(min_length=1)
|
workspace_id: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|||||||
+306
-109
@@ -7,7 +7,9 @@ import pytest
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from tests.wf_mcp.test_support import echo_tool
|
from tests.wf_mcp.test_support import echo_tool
|
||||||
|
from wf_api.draft_authoring import DraftOutcomeRef, WorkflowDraftAuthoringApi
|
||||||
from wf_api.drafts import WorkflowDraftApi
|
from wf_api.drafts import WorkflowDraftApi
|
||||||
|
from wf_api.service import WorkflowApi
|
||||||
from wf_artifacts import FileDraftWorkspaceStore, FileWorkflowArtifactStore
|
from wf_artifacts import FileDraftWorkspaceStore, FileWorkflowArtifactStore
|
||||||
from wf_authoring import node
|
from wf_authoring import node
|
||||||
from wf_mcp.broker import WfMcpService
|
from wf_mcp.broker import WfMcpService
|
||||||
@@ -65,7 +67,7 @@ class _SnapshotInput(BaseModel):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@node(name="snapshot_tool")
|
@node(name="snapshot_tool", outcomes=("ok", "skipped"))
|
||||||
def _snapshot_tool(payload: _SnapshotInput) -> _SnapshotOutput:
|
def _snapshot_tool(payload: _SnapshotInput) -> _SnapshotOutput:
|
||||||
return _SnapshotOutput(after=_Snapshot(clicked=True))
|
return _SnapshotOutput(after=_Snapshot(clicked=True))
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ def _draft_api(
|
|||||||
artifact_store: FileWorkflowArtifactStore,
|
artifact_store: FileWorkflowArtifactStore,
|
||||||
*,
|
*,
|
||||||
register_echo: bool = False,
|
register_echo: bool = False,
|
||||||
) -> tuple[WorkflowDraftApi, WfMcpService]:
|
) -> tuple[WorkflowDraftApi, WfMcpService, WorkflowDraftAuthoringApi]:
|
||||||
mcp_root = artifact_store.root / "drafts_mcp" / str(id(artifact_store))
|
mcp_root = artifact_store.root / "drafts_mcp" / str(id(artifact_store))
|
||||||
service = WfMcpService(
|
service = WfMcpService(
|
||||||
store=FileStore(mcp_root),
|
store=FileStore(mcp_root),
|
||||||
@@ -87,13 +89,17 @@ def _draft_api(
|
|||||||
)
|
)
|
||||||
service.register_specs("demo.personal", echo_tool)
|
service.register_specs("demo.personal", echo_tool)
|
||||||
context = context_from_service(service)
|
context = context_from_service(service)
|
||||||
return WorkflowDraftApi(context), service
|
return (
|
||||||
|
WorkflowDraftApi(context),
|
||||||
|
service,
|
||||||
|
WorkflowDraftAuthoringApi(context, WorkflowDraftApi(context)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_patch_draft_applies_json_patch(tmp_path: Path) -> None:
|
async def test_patch_draft_applies_json_patch(tmp_path: Path) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch")
|
||||||
api, _service = _draft_api(artifact_store, register_echo=True)
|
api, _service, _authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
|
||||||
result = await api.patch_draft(
|
result = await api.patch_draft(
|
||||||
draft=_echo_draft(),
|
draft=_echo_draft(),
|
||||||
@@ -116,7 +122,7 @@ async def test_patch_draft_applies_json_patch(tmp_path: Path) -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_draft_workspace_creates_workspace(tmp_path: Path) -> None:
|
async def test_create_draft_workspace_creates_workspace(tmp_path: Path) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_workspace")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_workspace")
|
||||||
api, _service = _draft_api(artifact_store)
|
api, _service, _authoring = _draft_api(artifact_store)
|
||||||
|
|
||||||
result = await api.create_draft_workspace(
|
result = await api.create_draft_workspace(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
@@ -138,7 +144,7 @@ async def test_list_draft_workspaces_returns_sorted_summaries_without_drafts(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_list_workspaces")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_list_workspaces")
|
||||||
api, _service = _draft_api(artifact_store)
|
api, _service, _authoring = _draft_api(artifact_store)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="b_draft",
|
workspace_id="b_draft",
|
||||||
title="B Draft",
|
title="B Draft",
|
||||||
@@ -164,7 +170,7 @@ async def test_list_draft_workspaces_returns_sorted_summaries_without_drafts(
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_delete_draft_workspace_is_idempotent(tmp_path: Path) -> None:
|
async def test_delete_draft_workspace_is_idempotent(tmp_path: Path) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_delete_workspace")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_delete_workspace")
|
||||||
api, _service = _draft_api(artifact_store)
|
api, _service, _authoring = _draft_api(artifact_store)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
draft=_echo_draft(),
|
draft=_echo_draft(),
|
||||||
@@ -186,7 +192,7 @@ async def test_delete_draft_workspace_is_idempotent(tmp_path: Path) -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_patch_draft_workspace_updates_revision(tmp_path: Path) -> None:
|
async def test_patch_draft_workspace_updates_revision(tmp_path: Path) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_workspace")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_workspace")
|
||||||
api, _service = _draft_api(artifact_store, register_echo=True)
|
api, _service, _authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
draft=_echo_draft(),
|
draft=_echo_draft(),
|
||||||
@@ -207,7 +213,7 @@ async def test_draft_workspace_patch_helpers_update_revision_and_bindings(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_helpers")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_helpers")
|
||||||
api, _service = _draft_api(artifact_store)
|
api, _service, _authoring = _draft_api(artifact_store)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
draft=_echo_draft(),
|
draft=_echo_draft(),
|
||||||
@@ -263,7 +269,7 @@ async def test_draft_workspace_patch_helpers_update_revision_and_bindings(
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_step_map_helpers_merge_with_existing_bindings(tmp_path: Path) -> None:
|
async def test_step_map_helpers_merge_with_existing_bindings(tmp_path: Path) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_helper_merge")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_helper_merge")
|
||||||
api, _service = _draft_api(artifact_store)
|
api, _service, _authoring = _draft_api(artifact_store)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
draft=_echo_draft(),
|
draft=_echo_draft(),
|
||||||
@@ -315,7 +321,7 @@ async def test_step_map_helpers_merge_with_existing_bindings(tmp_path: Path) ->
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_validate_draft_workspace_refreshes_status(tmp_path: Path) -> None:
|
async def test_validate_draft_workspace_refreshes_status(tmp_path: Path) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_validate_workspace")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_validate_workspace")
|
||||||
api, service = _draft_api(artifact_store, register_echo=True)
|
api, service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
draft = _echo_draft()
|
draft = _echo_draft()
|
||||||
draft["routes"]["echo"] = {"typo": "__end__"}
|
draft["routes"]["echo"] = {"typo": "__end__"}
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
@@ -340,7 +346,7 @@ async def test_validate_draft_workspace_suggests_bind_output_to_state(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_repair_hint")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_repair_hint")
|
||||||
api, service = _draft_api(artifact_store)
|
api, service, authoring = _draft_api(artifact_store)
|
||||||
service.register_connection(
|
service.register_connection(
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
||||||
)
|
)
|
||||||
@@ -385,7 +391,7 @@ async def test_patch_draft_workspace_validates_new_use_step_with_context_specs(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_new_use")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_patch_new_use")
|
||||||
api, service = _draft_api(artifact_store, register_echo=True)
|
api, service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
|
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
@@ -438,9 +444,9 @@ async def test_create_minimal_draft_workspace_minimal_success_path(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_minimal_workspace")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_minimal_workspace")
|
||||||
api, _service = _draft_api(artifact_store, register_echo=True)
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
|
||||||
result = await api.create_minimal_draft_workspace(
|
result = await authoring.create_minimal_draft_workspace(
|
||||||
workspace_id="echo_minimal",
|
workspace_id="echo_minimal",
|
||||||
name="echo",
|
name="echo",
|
||||||
capability_name="demo.personal.echo_tool",
|
capability_name="demo.personal.echo_tool",
|
||||||
@@ -498,92 +504,40 @@ async def test_delegation_smoke_validate_draft_equivalence(tmp_path: Path) -> No
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_state_schema_from_output_copies_output_property_defs(
|
async def test_facade_delegates_semantic_authoring_to_authoring_service(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_state_from_output")
|
"""WorkflowApi constructs a sibling WorkflowDraftAuthoringApi."""
|
||||||
api, service = _draft_api(artifact_store)
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_facade_delegation")
|
||||||
|
mcp_root = artifact_store.root / "facade_mcp"
|
||||||
|
service = WfMcpService(
|
||||||
|
store=FileStore(mcp_root),
|
||||||
|
artifact_store=artifact_store,
|
||||||
|
draft_workspace_store=FileDraftWorkspaceStore(mcp_root),
|
||||||
|
)
|
||||||
service.register_connection(
|
service.register_connection(
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
||||||
)
|
)
|
||||||
service.register_specs("demo.personal", _snapshot_tool)
|
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
|
||||||
await api.create_draft_workspace(
|
|
||||||
workspace_id="snapshot_ws",
|
|
||||||
draft={
|
|
||||||
"name": "snapshot",
|
|
||||||
"input_schema": {"type": "object", "properties": {}},
|
|
||||||
"state_schema": {"type": "object", "properties": {}},
|
|
||||||
"output_schema": {"type": "object", "properties": {}},
|
|
||||||
"start": "snap",
|
|
||||||
"steps": {
|
|
||||||
"snap": {
|
|
||||||
"use": "demo.personal.snapshot_tool",
|
|
||||||
"input": [],
|
|
||||||
"output": [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"routes": {"snap": {"ok": "__end__"}},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
updated = await api.add_state_schema_from_output(
|
context = context_from_service(service)
|
||||||
workspace_id="snapshot_ws",
|
facade = WorkflowApi(context)
|
||||||
|
|
||||||
|
assert facade.draft_authoring is not None
|
||||||
|
assert isinstance(facade.draft_authoring, WorkflowDraftAuthoringApi)
|
||||||
|
|
||||||
|
await facade.create_draft_workspace(
|
||||||
|
workspace_id="ws1",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
result = await facade.bind_output_to_state(
|
||||||
|
workspace_id="ws1",
|
||||||
revision=1,
|
revision=1,
|
||||||
step_id="snap",
|
step_id="echo",
|
||||||
output_field="after",
|
output_field="echoed",
|
||||||
state_path="state.after",
|
state_path="state.echoed",
|
||||||
)
|
)
|
||||||
fetched = await api.get_draft_workspace(
|
assert result["revision"] == 2
|
||||||
workspace_id="snapshot_ws",
|
|
||||||
include_draft=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
state_schema = fetched["draft"]["state_schema"]
|
|
||||||
assert updated["revision"] == 2
|
|
||||||
assert state_schema["properties"]["after"]["$ref"] == "#/$defs/_Snapshot"
|
|
||||||
assert state_schema["$defs"]["_Snapshot"]["properties"]["clicked"] == {
|
|
||||||
"title": "Clicked",
|
|
||||||
"type": "boolean",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_add_state_schema_from_output_rejects_nested_state_path(
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_nested_state_output")
|
|
||||||
api, service = _draft_api(artifact_store)
|
|
||||||
service.register_connection(
|
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
||||||
)
|
|
||||||
service.register_specs("demo.personal", _snapshot_tool)
|
|
||||||
await api.create_draft_workspace(
|
|
||||||
workspace_id="snapshot_ws",
|
|
||||||
draft={
|
|
||||||
"name": "snapshot",
|
|
||||||
"input_schema": {"type": "object", "properties": {}},
|
|
||||||
"state_schema": {"type": "object", "properties": {}},
|
|
||||||
"output_schema": {"type": "object", "properties": {}},
|
|
||||||
"start": "snap",
|
|
||||||
"steps": {
|
|
||||||
"snap": {
|
|
||||||
"use": "demo.personal.snapshot_tool",
|
|
||||||
"input": [],
|
|
||||||
"output": [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"routes": {"snap": {"ok": "__end__"}},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="state_path must name one root field"):
|
|
||||||
await api.add_state_schema_from_output(
|
|
||||||
workspace_id="snapshot_ws",
|
|
||||||
revision=1,
|
|
||||||
step_id="snap",
|
|
||||||
output_field="after",
|
|
||||||
state_path="state.after.clicked",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -591,7 +545,7 @@ async def test_bind_output_to_state_projects_schema_and_merges_output_map(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_output_state")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_output_state")
|
||||||
api, service = _draft_api(artifact_store)
|
api, service, authoring = _draft_api(artifact_store)
|
||||||
service.register_connection(
|
service.register_connection(
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
||||||
)
|
)
|
||||||
@@ -623,7 +577,7 @@ async def test_bind_output_to_state_projects_schema_and_merges_output_map(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
updated = await api.bind_output_to_state(
|
updated = await authoring.bind_output_to_state(
|
||||||
workspace_id="snapshot_ws",
|
workspace_id="snapshot_ws",
|
||||||
revision=1,
|
revision=1,
|
||||||
step_id="snap",
|
step_id="snap",
|
||||||
@@ -657,7 +611,7 @@ async def test_bind_output_to_state_projects_schema_and_merges_output_map(
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_bind_output_to_state_rejects_nested_state_path(tmp_path: Path) -> None:
|
async def test_bind_output_to_state_rejects_nested_state_path(tmp_path: Path) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_nested_state")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_nested_state")
|
||||||
api, service = _draft_api(artifact_store)
|
api, service, authoring = _draft_api(artifact_store)
|
||||||
service.register_connection(
|
service.register_connection(
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
||||||
)
|
)
|
||||||
@@ -682,7 +636,7 @@ async def test_bind_output_to_state_rejects_nested_state_path(tmp_path: Path) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="state_path must name one root field"):
|
with pytest.raises(ValueError, match="state_path must name one root field"):
|
||||||
await api.bind_output_to_state(
|
await authoring.bind_output_to_state(
|
||||||
workspace_id="snapshot_ws",
|
workspace_id="snapshot_ws",
|
||||||
revision=1,
|
revision=1,
|
||||||
step_id="snap",
|
step_id="snap",
|
||||||
@@ -696,7 +650,7 @@ async def test_bind_output_to_state_rejects_missing_output_field(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_missing_output")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_missing_output")
|
||||||
api, service = _draft_api(artifact_store)
|
api, service, authoring = _draft_api(artifact_store)
|
||||||
service.register_connection(
|
service.register_connection(
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
||||||
)
|
)
|
||||||
@@ -721,7 +675,7 @@ async def test_bind_output_to_state_rejects_missing_output_field(
|
|||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="output field 'missing'"):
|
with pytest.raises(ValueError, match="output field 'missing'"):
|
||||||
await api.bind_output_to_state(
|
await authoring.bind_output_to_state(
|
||||||
workspace_id="snapshot_ws",
|
workspace_id="snapshot_ws",
|
||||||
revision=1,
|
revision=1,
|
||||||
step_id="snap",
|
step_id="snap",
|
||||||
@@ -735,7 +689,7 @@ async def test_bind_output_to_state_rejects_step_without_capability_use(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_no_use")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_bind_no_use")
|
||||||
api, _service = _draft_api(artifact_store)
|
api, _service, authoring = _draft_api(artifact_store)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="snapshot_ws",
|
workspace_id="snapshot_ws",
|
||||||
draft={
|
draft={
|
||||||
@@ -755,7 +709,7 @@ async def test_bind_output_to_state_rejects_step_without_capability_use(
|
|||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="does not declare a capability use"):
|
with pytest.raises(ValueError, match="does not declare a capability use"):
|
||||||
await api.bind_output_to_state(
|
await authoring.bind_output_to_state(
|
||||||
workspace_id="snapshot_ws",
|
workspace_id="snapshot_ws",
|
||||||
revision=1,
|
revision=1,
|
||||||
step_id="snap",
|
step_id="snap",
|
||||||
@@ -769,22 +723,21 @@ async def test_add_step_from_capability_wires_route_inputs_and_state_outputs(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_add_step")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_add_step")
|
||||||
api, service = _draft_api(artifact_store, register_echo=True)
|
api, service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
|
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
draft=_echo_draft(),
|
draft=_echo_draft(),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await api.add_step_from_capability(
|
result = await authoring.add_step_from_capability(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
revision=1,
|
revision=1,
|
||||||
step_id="snap",
|
step_id="snap",
|
||||||
capability_name="demo.personal.snapshot_tool",
|
capability_name="demo.personal.snapshot_tool",
|
||||||
route_from_step="echo",
|
route_from_step="echo",
|
||||||
route_from_outcome="ok",
|
route_from_outcome="ok",
|
||||||
route_outcome="ok",
|
routes={"ok": "__end__", "skipped": "__end__"},
|
||||||
route_to="__end__",
|
|
||||||
input_map={},
|
input_map={},
|
||||||
bind_outputs={"after": "state.after"},
|
bind_outputs={"after": "state.after"},
|
||||||
)
|
)
|
||||||
@@ -814,22 +767,266 @@ async def test_add_step_from_capability_rejects_existing_step_id(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_add_step_duplicate")
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_add_step_duplicate")
|
||||||
api, _service = _draft_api(artifact_store, register_echo=True)
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
await api.create_draft_workspace(
|
await api.create_draft_workspace(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
draft=_echo_draft(),
|
draft=_echo_draft(),
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="draft step 'echo' already exists"):
|
with pytest.raises(ValueError, match="draft step 'echo' already exists"):
|
||||||
await api.add_step_from_capability(
|
await authoring.add_step_from_capability(
|
||||||
workspace_id="echo_ws",
|
workspace_id="echo_ws",
|
||||||
revision=1,
|
revision=1,
|
||||||
step_id="echo",
|
step_id="echo",
|
||||||
capability_name="demo.personal.echo_tool",
|
capability_name="demo.personal.echo_tool",
|
||||||
route_from_step=None,
|
route_from_step=None,
|
||||||
route_from_outcome="ok",
|
route_from_outcome="ok",
|
||||||
route_outcome="ok",
|
routes={"ok": "__end__"},
|
||||||
route_to="__end__",
|
|
||||||
input_map={},
|
input_map={},
|
||||||
bind_outputs={},
|
bind_outputs={},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_branch_draft_updates_routes_atomically(tmp_path: Path) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_branch")
|
||||||
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="branching",
|
||||||
|
draft={
|
||||||
|
"name": "branching",
|
||||||
|
"input_schema": {"type": "object", "properties": {}},
|
||||||
|
"state_schema": {"type": "object", "properties": {}},
|
||||||
|
"output_schema": {"type": "object", "properties": {}},
|
||||||
|
"start": "classify",
|
||||||
|
"steps": {
|
||||||
|
"classify": {
|
||||||
|
"use": "demo.personal.echo_tool",
|
||||||
|
"input": [],
|
||||||
|
"output": [],
|
||||||
|
},
|
||||||
|
"tool_error": {
|
||||||
|
"use": "demo.personal.echo_tool",
|
||||||
|
"input": [],
|
||||||
|
"output": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"routes": {
|
||||||
|
"classify": {"ok": "classify"},
|
||||||
|
"tool_error": {"ok": "__end__"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await authoring.branch_draft(
|
||||||
|
workspace_id="branching",
|
||||||
|
revision=1,
|
||||||
|
step_id="classify",
|
||||||
|
routes={"ok": "classify", "error": "tool_error"},
|
||||||
|
)
|
||||||
|
assert result["revision"] == 2
|
||||||
|
workspace = await api.get_draft_workspace(
|
||||||
|
workspace_id="branching", include_draft=True
|
||||||
|
)
|
||||||
|
assert workspace["draft"]["routes"]["classify"] == {
|
||||||
|
"ok": "classify",
|
||||||
|
"error": "tool_error",
|
||||||
|
}
|
||||||
|
assert "tool_error" in workspace["draft"]["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_handle_draft_updates_multiple_source_outcomes(tmp_path: Path) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_handle")
|
||||||
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="handling",
|
||||||
|
draft={
|
||||||
|
"name": "handling",
|
||||||
|
"input_schema": {"type": "object", "properties": {}},
|
||||||
|
"state_schema": {"type": "object", "properties": {}},
|
||||||
|
"output_schema": {"type": "object", "properties": {}},
|
||||||
|
"start": "lookup",
|
||||||
|
"steps": {
|
||||||
|
"lookup": {
|
||||||
|
"use": "demo.personal.echo_tool",
|
||||||
|
"input": [],
|
||||||
|
"output": [],
|
||||||
|
},
|
||||||
|
"transform": {
|
||||||
|
"use": "demo.personal.echo_tool",
|
||||||
|
"input": [],
|
||||||
|
"output": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"routes": {
|
||||||
|
"lookup": {"ok": "transform", "error": "lookup"},
|
||||||
|
"transform": {"ok": "__end__", "error": "transform"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await authoring.handle_draft(
|
||||||
|
workspace_id="handling",
|
||||||
|
revision=1,
|
||||||
|
branches=[
|
||||||
|
DraftOutcomeRef(step_id="lookup", outcome="error"),
|
||||||
|
DraftOutcomeRef(step_id="transform", outcome="error"),
|
||||||
|
],
|
||||||
|
target="__end__",
|
||||||
|
)
|
||||||
|
assert result["revision"] == 2
|
||||||
|
workspace = await api.get_draft_workspace(
|
||||||
|
workspace_id="handling", include_draft=True
|
||||||
|
)
|
||||||
|
assert workspace["draft"]["routes"]["lookup"]["error"] == "__end__"
|
||||||
|
assert workspace["draft"]["routes"]["transform"]["error"] == "__end__"
|
||||||
|
assert workspace["draft"]["routes"]["lookup"]["ok"] == "transform"
|
||||||
|
assert workspace["draft"]["routes"]["transform"]["ok"] == "__end__"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_branch_draft_no_change_when_routes_unchanged(tmp_path: Path) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_branch_noop")
|
||||||
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="noop_ws",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
before = await api.get_draft_workspace(workspace_id="noop_ws", include_draft=True)
|
||||||
|
|
||||||
|
result = await authoring.branch_draft(
|
||||||
|
workspace_id="noop_ws",
|
||||||
|
revision=1,
|
||||||
|
step_id="echo",
|
||||||
|
routes={"ok": "__end__"},
|
||||||
|
)
|
||||||
|
after = await api.get_draft_workspace(workspace_id="noop_ws", include_draft=True)
|
||||||
|
assert result["revision"] == 1
|
||||||
|
assert after == before
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_handle_draft_empty_branches_noop(tmp_path: Path) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_handle_noop")
|
||||||
|
api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="noop_ws",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
before = await api.get_draft_workspace(workspace_id="noop_ws", include_draft=True)
|
||||||
|
|
||||||
|
result = await authoring.handle_draft(
|
||||||
|
workspace_id="noop_ws",
|
||||||
|
revision=1,
|
||||||
|
branches=[],
|
||||||
|
target="fail",
|
||||||
|
)
|
||||||
|
after = await api.get_draft_workspace(workspace_id="noop_ws", include_draft=True)
|
||||||
|
assert result["revision"] == 1
|
||||||
|
assert after == before
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_step_from_capability_infers_single_outcome_route(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""One declared outcome named 'done', no routes supplied -> routes to __end__."""
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_single_outcome")
|
||||||
|
api, service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
service.register_specs("demo.personal", echo_tool)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="single",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await authoring.add_step_from_capability(
|
||||||
|
workspace_id="single",
|
||||||
|
revision=1,
|
||||||
|
step_id="done_step",
|
||||||
|
capability_name="demo.personal.echo_tool",
|
||||||
|
)
|
||||||
|
assert result["revision"] == 2
|
||||||
|
workspace = await api.get_draft_workspace(workspace_id="single", include_draft=True)
|
||||||
|
assert workspace["draft"]["routes"]["done_step"] == {"ok": "__end__"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_step_from_capability_requires_complete_routes_for_multi_outcome(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Multiple declared outcomes with incomplete explicit routes raises ValueError."""
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_multi_outcome")
|
||||||
|
api, service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="multi",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="missing routes"):
|
||||||
|
await authoring.add_step_from_capability(
|
||||||
|
workspace_id="multi",
|
||||||
|
revision=1,
|
||||||
|
step_id="snap",
|
||||||
|
capability_name="demo.personal.snapshot_tool",
|
||||||
|
routes={"ok": "__end__"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_step_from_capability_rejects_unknown_routes_for_multi_outcome(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_unknown_outcome")
|
||||||
|
api, service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
service.register_specs("demo.personal", _snapshot_tool)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="unknown_multi",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="unknown routes"):
|
||||||
|
await authoring.add_step_from_capability(
|
||||||
|
workspace_id="unknown_multi",
|
||||||
|
revision=1,
|
||||||
|
step_id="snap",
|
||||||
|
capability_name="demo.personal.snapshot_tool",
|
||||||
|
routes={"ok": "__end__", "skipped": "__end__", "typo": "__end__"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_compile_draft_workspace_returns_compiled_plan(tmp_path: Path) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_compile")
|
||||||
|
api, _service, _authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="compile_me",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
)
|
||||||
|
before = await api.get_draft_workspace(
|
||||||
|
workspace_id="compile_me", include_draft=True
|
||||||
|
)
|
||||||
|
result = await api.compile_draft_workspace(workspace_id="compile_me")
|
||||||
|
after = await api.get_draft_workspace(workspace_id="compile_me", include_draft=True)
|
||||||
|
assert result["compiled_plan"]["name"] == "echo"
|
||||||
|
assert result["required_capabilities"]
|
||||||
|
assert after == before
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_compile_draft_workspace_invalid_returns_diagnostics(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_compile_invalid")
|
||||||
|
api, _service, _authoring = _draft_api(artifact_store, register_echo=True)
|
||||||
|
draft = _echo_draft()
|
||||||
|
draft["routes"]["echo"] = {"typo": "__end__"}
|
||||||
|
await api.create_draft_workspace(
|
||||||
|
workspace_id="invalid_ws",
|
||||||
|
draft=draft,
|
||||||
|
)
|
||||||
|
result = await api.compile_draft_workspace(workspace_id="invalid_ws")
|
||||||
|
assert result["status"] == "invalid"
|
||||||
|
assert "compiled_plan" not in result
|
||||||
|
assert result["diagnostics"]
|
||||||
|
|||||||
@@ -144,16 +144,6 @@ def test_wf_draft_map_help_explains_replace_merge_and_validate() -> None:
|
|||||||
assert "draft validate" in output_help
|
assert "draft validate" in output_help
|
||||||
|
|
||||||
|
|
||||||
def test_wf_draft_add_state_from_output_help_explains_schema_copy() -> None:
|
|
||||||
result = runner.invoke(app, ["draft", "add-state-from-output", "--help"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
help_text = " ".join(result.output.split())
|
|
||||||
assert "capability output field schema" in help_text
|
|
||||||
assert "$defs" in help_text
|
|
||||||
assert "draft validate" in help_text
|
|
||||||
|
|
||||||
|
|
||||||
def test_wf_draft_bind_output_to_state_help_explains_composed_edit() -> None:
|
def test_wf_draft_bind_output_to_state_help_explains_composed_edit() -> None:
|
||||||
result = runner.invoke(app, ["draft", "bind-output-to-state", "--help"])
|
result = runner.invoke(app, ["draft", "bind-output-to-state", "--help"])
|
||||||
|
|
||||||
|
|||||||
@@ -1101,23 +1101,6 @@ def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) ->
|
|||||||
"--merge",
|
"--merge",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
state_added = runner.invoke(
|
|
||||||
app,
|
|
||||||
[
|
|
||||||
*base_args,
|
|
||||||
"draft",
|
|
||||||
"add-state-from-output",
|
|
||||||
"focused_ws",
|
|
||||||
"--revision",
|
|
||||||
"7",
|
|
||||||
"--step",
|
|
||||||
"call",
|
|
||||||
"--output",
|
|
||||||
"value",
|
|
||||||
"--state",
|
|
||||||
"state.extra_value",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
inspected = runner.invoke(
|
inspected = runner.invoke(
|
||||||
app,
|
app,
|
||||||
[*base_args, "draft", "inspect", "focused_ws", "--include-draft"],
|
[*base_args, "draft", "inspect", "focused_ws", "--include-draft"],
|
||||||
@@ -1129,7 +1112,6 @@ def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) ->
|
|||||||
assert output_mapped.exit_code == 0, output_mapped.output
|
assert output_mapped.exit_code == 0, output_mapped.output
|
||||||
assert input_merged.exit_code == 0, input_merged.output
|
assert input_merged.exit_code == 0, input_merged.output
|
||||||
assert output_merged.exit_code == 0, output_merged.output
|
assert output_merged.exit_code == 0, output_merged.output
|
||||||
assert state_added.exit_code == 0, state_added.output
|
|
||||||
assert inspected.exit_code == 0, inspected.output
|
assert inspected.exit_code == 0, inspected.output
|
||||||
payload = json.loads(inspected.output)
|
payload = json.loads(inspected.output)
|
||||||
draft = payload["draft"]
|
draft = payload["draft"]
|
||||||
@@ -1137,25 +1119,24 @@ def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) ->
|
|||||||
assert draft["routes"]["call"]["ok"] == "__end__"
|
assert draft["routes"]["call"]["ok"] == "__end__"
|
||||||
assert draft["steps"]["call"]["input"] == [
|
assert draft["steps"]["call"]["input"] == [
|
||||||
{
|
{
|
||||||
"target": {"root": "local", "parts": ["value"]},
|
"target": "value",
|
||||||
"path": {"root": "input", "parts": ["value"]},
|
"path": "input.value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"target": {"root": "local", "parts": ["extra"]},
|
"target": "extra",
|
||||||
"path": {"root": "input", "parts": ["extra"]},
|
"path": "input.extra",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
assert draft["steps"]["call"]["output"] == [
|
assert draft["steps"]["call"]["output"] == [
|
||||||
{
|
{
|
||||||
"source": {"root": "local", "parts": ["value"]},
|
"source": "value",
|
||||||
"target": {"root": "state", "parts": ["value"]},
|
"target": "state.value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": {"root": "local", "parts": ["extra"]},
|
"source": "extra",
|
||||||
"target": {"root": "state", "parts": ["extra"]},
|
"target": "state.extra",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
assert "extra_value" in draft["state_schema"]["properties"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_wf_draft_bind_output_to_state_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
def test_wf_draft_bind_output_to_state_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||||
@@ -1244,10 +1225,8 @@ def test_wf_draft_add_step_from_capability_uses_rpc_target(
|
|||||||
"call",
|
"call",
|
||||||
"--from-outcome",
|
"--from-outcome",
|
||||||
"ok",
|
"ok",
|
||||||
"--outcome",
|
"--route",
|
||||||
"ok",
|
"ok=__end__",
|
||||||
"--to",
|
|
||||||
"__end__",
|
|
||||||
"--input",
|
"--input",
|
||||||
"input.value=value",
|
"input.value=value",
|
||||||
"--bind-output",
|
"--bind-output",
|
||||||
@@ -1261,6 +1240,85 @@ def test_wf_draft_add_step_from_capability_uses_rpc_target(
|
|||||||
assert payload["status"] == "valid"
|
assert payload["status"] == "valid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_draft_compile_prints_compiled_plan(monkeypatch, tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
_patch_rpc_client_to_server(monkeypatch, server)
|
||||||
|
config_path = tmp_path / "wf.json"
|
||||||
|
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||||
|
runner = CliRunner()
|
||||||
|
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
|
||||||
|
|
||||||
|
created = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
*base_args,
|
||||||
|
"draft",
|
||||||
|
"create-from-capability",
|
||||||
|
"compile_ws",
|
||||||
|
"wf.std.constant",
|
||||||
|
"--name",
|
||||||
|
"compile_me",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert created.exit_code == 0, created.output
|
||||||
|
|
||||||
|
result = runner.invoke(app, [*base_args, "draft", "compile", "compile_ws"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
payload = json.loads(result.output)
|
||||||
|
assert payload["name"] == "compile_me"
|
||||||
|
assert "compiled_plan" not in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_draft_compile_invalid_prints_diagnostics_to_stderr(
|
||||||
|
monkeypatch, tmp_path
|
||||||
|
) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
asyncio.run(
|
||||||
|
server.api.create_draft_workspace(
|
||||||
|
workspace_id="invalid_compile_ws",
|
||||||
|
draft={
|
||||||
|
"name": "invalid_compile",
|
||||||
|
"input_schema": {"type": "object"},
|
||||||
|
"state_schema": {"type": "object", "properties": {}},
|
||||||
|
"output_schema": {"type": "object", "properties": {}},
|
||||||
|
"start": "call",
|
||||||
|
"steps": {
|
||||||
|
"call": {
|
||||||
|
"use": "wf.std.constant",
|
||||||
|
"input": [],
|
||||||
|
"output": [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": {"call": {"typo": "__end__"}},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_patch_rpc_client_to_server(monkeypatch, server)
|
||||||
|
config_path = tmp_path / "wf.json"
|
||||||
|
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
"--config",
|
||||||
|
str(config_path),
|
||||||
|
"--url",
|
||||||
|
"http://test/rpc",
|
||||||
|
"draft",
|
||||||
|
"compile",
|
||||||
|
"invalid_compile_ws",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 1
|
||||||
|
# This Typer test runner mixes stderr into output; the command implementation
|
||||||
|
# writes invalid compile diagnostics with err=True for real terminals.
|
||||||
|
assert '"status": "invalid"' in result.output
|
||||||
|
assert "compiled_plan" not in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_wf_deploy_create_alias_saves_deployment(monkeypatch, tmp_path) -> None:
|
def test_wf_deploy_create_alias_saves_deployment(monkeypatch, tmp_path) -> None:
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
assert "wf.workflow.set_draft_route" in names
|
assert "wf.workflow.set_draft_route" in names
|
||||||
assert "wf.workflow.set_step_input_map" in names
|
assert "wf.workflow.set_step_input_map" in names
|
||||||
assert "wf.workflow.set_step_output_map" in names
|
assert "wf.workflow.set_step_output_map" in names
|
||||||
assert "wf.workflow.add_state_from_output" in names
|
|
||||||
assert "wf.workflow.bind_output_to_state" in names
|
assert "wf.workflow.bind_output_to_state" in names
|
||||||
assert "wf.workflow.add_step_from_capability" in names
|
assert "wf.workflow.add_step_from_capability" in names
|
||||||
assert "wf.workflow.create_minimal_draft_workspace" in names
|
assert "wf.workflow.create_minimal_draft_workspace" in names
|
||||||
@@ -115,12 +114,6 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
].inputSchema
|
].inputSchema
|
||||||
set_input_request = set_input_schema["properties"]["request"]
|
set_input_request = set_input_schema["properties"]["request"]
|
||||||
assert "merge" in set_input_request["properties"]
|
assert "merge" in set_input_request["properties"]
|
||||||
state_from_output_schema = tools_by_name[
|
|
||||||
"wf.workflow.add_state_from_output"
|
|
||||||
].inputSchema
|
|
||||||
state_request = state_from_output_schema["properties"]["request"]
|
|
||||||
assert "output_field" in state_request["properties"]
|
|
||||||
assert "state_path" in state_request["properties"]
|
|
||||||
add_step_schema = tools_by_name[
|
add_step_schema = tools_by_name[
|
||||||
"wf.workflow.add_step_from_capability"
|
"wf.workflow.add_step_from_capability"
|
||||||
].inputSchema
|
].inputSchema
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def test_workflow_surface_rejects_unknown_draft_route_outcome_when_spec_is_known
|
|||||||
payload = asyncio.run(h.validate_draft(draft=draft))
|
payload = asyncio.run(h.validate_draft(draft=draft))
|
||||||
|
|
||||||
assert payload["status"] == "invalid"
|
assert payload["status"] == "invalid"
|
||||||
assert payload["diagnostics"][0]["path"] == "routes.echo.typo"
|
assert payload["diagnostics"][0]["path"] == "edges[0].outcome"
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_creates_artifact_from_draft_with_binding_suggestions(
|
def test_workflow_surface_creates_artifact_from_draft_with_binding_suggestions(
|
||||||
@@ -150,7 +150,7 @@ def test_workflow_surface_validates_draft_workspace_with_live_outcomes(
|
|||||||
|
|
||||||
assert payload["revision"] == 1
|
assert payload["revision"] == 1
|
||||||
assert payload["status"] == "invalid"
|
assert payload["status"] == "invalid"
|
||||||
assert payload["diagnostics"][0]["code"] == "unknown_outcome"
|
assert payload["diagnostics"][0]["code"] == "undeclared_edge_outcome"
|
||||||
assert fetched["status"] == "invalid"
|
assert fetched["status"] == "invalid"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ async def test_rpc_draft_workspace_methods(tmp_path) -> None:
|
|||||||
"name": "remote_constant",
|
"name": "remote_constant",
|
||||||
"title": "Remote Constant",
|
"title": "Remote Constant",
|
||||||
"input_map": {},
|
"input_map": {},
|
||||||
"output_map": {"value": "state.result"},
|
"output_map": {},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
listed = await _rpc(client, "workflow.draft_workspaces.list", {})
|
listed = await _rpc(client, "workflow.draft_workspaces.list", {})
|
||||||
@@ -722,23 +722,12 @@ async def test_rpc_draft_workspace_focused_edit_methods(tmp_path) -> None:
|
|||||||
"merge": True,
|
"merge": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
state_added = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.draft_workspaces.add_state_from_output",
|
|
||||||
{
|
|
||||||
"workspace_id": "focused_ws",
|
|
||||||
"revision": 7,
|
|
||||||
"step_id": "call",
|
|
||||||
"output_field": "value",
|
|
||||||
"state_path": "state.extra_value",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
state_bound = await _rpc(
|
state_bound = await _rpc(
|
||||||
client,
|
client,
|
||||||
"workflow.draft_workspaces.bind_output_to_state",
|
"workflow.draft_workspaces.bind_output_to_state",
|
||||||
{
|
{
|
||||||
"workspace_id": "focused_ws",
|
"workspace_id": "focused_ws",
|
||||||
"revision": state_added["result"]["revision"],
|
"revision": 7,
|
||||||
"step_id": "call",
|
"step_id": "call",
|
||||||
"output_field": "value",
|
"output_field": "value",
|
||||||
"state_path": "state.extra_value",
|
"state_path": "state.extra_value",
|
||||||
@@ -756,8 +745,7 @@ async def test_rpc_draft_workspace_focused_edit_methods(tmp_path) -> None:
|
|||||||
assert output_mapped["result"]["revision"] == 5
|
assert output_mapped["result"]["revision"] == 5
|
||||||
assert input_merged["result"]["revision"] == 6
|
assert input_merged["result"]["revision"] == 6
|
||||||
assert output_merged["result"]["revision"] == 7
|
assert output_merged["result"]["revision"] == 7
|
||||||
assert state_added["result"]["revision"] == 8
|
assert state_bound["result"]["revision"] == 8
|
||||||
assert state_bound["result"]["revision"] == 9
|
|
||||||
draft = fetched["result"]["draft"]
|
draft = fetched["result"]["draft"]
|
||||||
assert draft["name"] == "focused_renamed"
|
assert draft["name"] == "focused_renamed"
|
||||||
assert draft["routes"]["call"]["ok"] == "__end__"
|
assert draft["routes"]["call"]["ok"] == "__end__"
|
||||||
@@ -812,8 +800,7 @@ async def test_rpc_draft_workspace_add_step_from_capability(tmp_path) -> None:
|
|||||||
"capability_name": "wf.std.constant",
|
"capability_name": "wf.std.constant",
|
||||||
"route_from_step": "call",
|
"route_from_step": "call",
|
||||||
"route_from_outcome": "ok",
|
"route_from_outcome": "ok",
|
||||||
"route_outcome": "ok",
|
"routes": {"ok": "__end__"},
|
||||||
"route_to": "__end__",
|
|
||||||
"input_map": {"input.value": "value"},
|
"input_map": {"input.value": "value"},
|
||||||
"bind_outputs": {"value": "state.second_value"},
|
"bind_outputs": {"value": "state.second_value"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ async def test_rpc_workflow_client_draft_workspace_lifecycle(tmp_path) -> None:
|
|||||||
name="client_constant",
|
name="client_constant",
|
||||||
title="Client Constant",
|
title="Client Constant",
|
||||||
input_map={},
|
input_map={},
|
||||||
output_map={"value": "state.result"},
|
output_map={},
|
||||||
)
|
)
|
||||||
listed = await client.list_draft_workspaces()
|
listed = await client.list_draft_workspaces()
|
||||||
fetched = await client.get_draft_workspace(workspace_id="client_ws")
|
fetched = await client.get_draft_workspace(workspace_id="client_ws")
|
||||||
@@ -501,16 +501,9 @@ async def test_rpc_client_draft_workspace_focused_edit_methods(tmp_path) -> None
|
|||||||
output_map={"extra": "state.extra"},
|
output_map={"extra": "state.extra"},
|
||||||
merge=True,
|
merge=True,
|
||||||
)
|
)
|
||||||
state_added = await client.add_state_schema_from_output(
|
|
||||||
workspace_id="client_focused_ws",
|
|
||||||
revision=7,
|
|
||||||
step_id="call",
|
|
||||||
output_field="value",
|
|
||||||
state_path="state.extra_value",
|
|
||||||
)
|
|
||||||
state_bound = await client.bind_output_to_state(
|
state_bound = await client.bind_output_to_state(
|
||||||
workspace_id="client_focused_ws",
|
workspace_id="client_focused_ws",
|
||||||
revision=state_added["revision"],
|
revision=7,
|
||||||
step_id="call",
|
step_id="call",
|
||||||
output_field="value",
|
output_field="value",
|
||||||
state_path="state.extra_value",
|
state_path="state.extra_value",
|
||||||
@@ -522,8 +515,7 @@ async def test_rpc_client_draft_workspace_focused_edit_methods(tmp_path) -> None
|
|||||||
assert output_mapped["revision"] == 5
|
assert output_mapped["revision"] == 5
|
||||||
assert input_merged["revision"] == 6
|
assert input_merged["revision"] == 6
|
||||||
assert output_merged["revision"] == 7
|
assert output_merged["revision"] == 7
|
||||||
assert state_added["revision"] == 8
|
assert state_bound["revision"] == 8
|
||||||
assert state_bound["revision"] == 9
|
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) -> None:
|
async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) -> None:
|
||||||
@@ -550,8 +542,7 @@ async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) ->
|
|||||||
capability_name="wf.std.constant",
|
capability_name="wf.std.constant",
|
||||||
route_from_step="call",
|
route_from_step="call",
|
||||||
route_from_outcome="ok",
|
route_from_outcome="ok",
|
||||||
route_outcome="ok",
|
routes={"ok": "__end__"},
|
||||||
route_to="__end__",
|
|
||||||
input_map={"input.value": "value"},
|
input_map={"input.value": "value"},
|
||||||
bind_outputs={"value": "state.second_value"},
|
bind_outputs={"value": "state.second_value"},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user