wf_artifacts to end this "with" usage
This commit is contained in:
@@ -265,12 +265,18 @@ arguments:
|
|||||||
"steps": {
|
"steps": {
|
||||||
"echo": {
|
"echo": {
|
||||||
"use": "demo.personal.echo_tool",
|
"use": "demo.personal.echo_tool",
|
||||||
"in": {
|
"input": [
|
||||||
"input.text": "text"
|
{
|
||||||
},
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
"out": {
|
"path": {"root": "input", "parts": ["text"]}
|
||||||
"echoed": "state.echoed"
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {
|
"routes": {
|
||||||
|
|||||||
+91
-51
@@ -69,12 +69,18 @@ A minimal draft looks like this:
|
|||||||
"steps": {
|
"steps": {
|
||||||
"echo": {
|
"echo": {
|
||||||
"use": "demo.personal.echo_tool",
|
"use": "demo.personal.echo_tool",
|
||||||
"in": {
|
"input": [
|
||||||
"input.text": "text"
|
{
|
||||||
},
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
"out": {
|
"path": {"root": "input", "parts": ["text"]}
|
||||||
"echoed": "state.echoed"
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {
|
"routes": {
|
||||||
@@ -95,44 +101,63 @@ Important details:
|
|||||||
- When saved with source bindings, concrete refs can be normalized to logical
|
- When saved with source bindings, concrete refs can be normalized to logical
|
||||||
refs such as `demo.echo_tool`.
|
refs such as `demo.echo_tool`.
|
||||||
|
|
||||||
## Mapping Shape
|
## Binding Shape
|
||||||
|
|
||||||
Draft maps are JSON objects whose keys and values are strings:
|
Draft `use` steps use the same canonical binding structs as core `NodeUse`:
|
||||||
|
|
||||||
```ts
|
```json
|
||||||
type InMap = Record<string, string>
|
{
|
||||||
type OutMap = Record<string, string>
|
"input": [
|
||||||
|
{
|
||||||
|
"target": {"root": "local", "parts": ["message"]},
|
||||||
|
"path": {"root": "input", "parts": ["text"]}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": {"root": "local", "parts": ["limit"]},
|
||||||
|
"value": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Both maps are source-to-destination.
|
Legacy draft maps `in`, `with`, and `out` are still accepted as parse-only
|
||||||
|
compatibility input. Valid drafts are saved and returned with canonical
|
||||||
| Map | Key | Value | Example |
|
`input` and `output` binding lists.
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `in` | graph source path | node-local input path | `"input.text": "message"` |
|
|
||||||
| `out` | node-local output path | graph state destination path | `"echoed": "state.echoed"` |
|
|
||||||
|
|
||||||
Draft `in` and `out` maps are an authoring-layer shape. When a draft is
|
|
||||||
compiled, the core workflow uses canonical `NodeUse.input` and
|
|
||||||
`NodeUse.output` binding lists. The old core `in_map`, `input_values`, and
|
|
||||||
`out_map` fields are parse-only compatibility inputs, not the preferred saved
|
|
||||||
shape.
|
|
||||||
|
|
||||||
Graph source paths in `in` normally start with `input.`, `state.`, or
|
Graph source paths in `in` normally start with `input.`, `state.`, or
|
||||||
`context.`. Node-local paths do not use those prefixes; they are paths inside
|
`context.`. Node-local paths do not use those prefixes; they are paths inside
|
||||||
the target capability's input or output payload.
|
the target capability's input or output payload.
|
||||||
|
|
||||||
For example:
|
For example, this canonical input/output pair:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"in": {
|
"input": [
|
||||||
"input.user.name": "user.name",
|
{
|
||||||
"state.job.title": "job.title"
|
"target": {"root": "local", "parts": ["user", "name"]},
|
||||||
|
"path": {"root": "input", "parts": ["user", "name"]}
|
||||||
},
|
},
|
||||||
"out": {
|
{
|
||||||
"user.age": "state.person.age",
|
"target": {"root": "local", "parts": ["job", "title"]},
|
||||||
"job.years": "state.experience.years"
|
"path": {"root": "state", "parts": ["job", "title"]}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["user", "age"]},
|
||||||
|
"target": {"root": "state", "parts": ["person", "age"]}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["job", "years"]},
|
||||||
|
"target": {"root": "state", "parts": ["experience", "years"]}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -147,28 +172,32 @@ Do not reverse the direction. This is wrong:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"in": {
|
"input": [
|
||||||
"message": "input.text"
|
{
|
||||||
|
"target": {"root": "input", "parts": ["text"]},
|
||||||
|
"path": {"root": "local", "parts": ["message"]}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
That asks the runtime to read from graph path `message` and write into a
|
That asks the runtime to read from graph path `message` and write into a
|
||||||
node-local input field literally named `input.text`.
|
node-local input field literally named `input.text`.
|
||||||
|
|
||||||
Do not put constants in `in`. This is also wrong:
|
Do not put constants in path bindings. This is wrong:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"in": {
|
"input": [
|
||||||
"value": {
|
{
|
||||||
"value": "CLICKED"
|
"target": {"root": "local", "parts": ["value"]},
|
||||||
}
|
"path": {"root": "input", "parts": ["CLICKED"]}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `with` for static node-local values instead.
|
Use an input value binding for static node-local values instead.
|
||||||
|
|
||||||
## Step Kinds
|
## Step Kinds
|
||||||
|
|
||||||
@@ -179,38 +208,49 @@ Calls a workflow capability.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"use": "demo.personal.echo_tool",
|
"use": "demo.personal.echo_tool",
|
||||||
"in": {
|
"input": [
|
||||||
"input.text": "text"
|
{
|
||||||
},
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
"out": {
|
"path": {"root": "input", "parts": ["text"]}
|
||||||
"echoed": "state.echoed"
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Use this for normal node calls, including generated workflow wrappers around
|
Use this for normal node calls, including generated workflow wrappers around
|
||||||
MCP tools and local `wf.std` capabilities.
|
MCP tools and local `wf.std` capabilities.
|
||||||
|
|
||||||
`use` steps can also provide static node-local input values with `with`.
|
`use` steps can also provide static node-local input values.
|
||||||
Use this for hardcoded strings, booleans, numbers, and small JSON values that
|
Use this for hardcoded strings, booleans, numbers, and small JSON values that
|
||||||
are part of the graph definition:
|
are part of the graph definition:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"use": "wf.std.constant",
|
"use": "wf.std.constant",
|
||||||
"with": {
|
"input": [
|
||||||
|
{
|
||||||
|
"target": {"root": "local", "parts": ["value"]},
|
||||||
"value": "CLICKED"
|
"value": "CLICKED"
|
||||||
},
|
|
||||||
"out": {
|
|
||||||
"value": "state.wait_text"
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["value"]},
|
||||||
|
"target": {"root": "state", "parts": ["wait_text"]}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Static values are not path mappings. Do not put `{"value": "CLICKED"}` inside
|
Static values are not path mappings. Use `{"target": ..., "value": ...}` for
|
||||||
`in`. The `in` object only maps graph paths such as `input.url` or
|
literal JSON values. Invalid draft step shapes are rejected instead of silently
|
||||||
`state.wait_text` to node-local input fields. Invalid draft step shapes are
|
compiling to `join`.
|
||||||
rejected instead of silently compiling to `join`.
|
|
||||||
|
|
||||||
Generated MCP tool wrappers are intentionally naive. They normally expose both
|
Generated MCP tool wrappers are intentionally naive. They normally expose both
|
||||||
`ok` and `error` outcomes, because MCP tool calls can report transport/provider
|
`ok` and `error` outcomes, because MCP tool calls can report transport/provider
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ from __future__ import annotations
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from wf_artifacts.drafts import patch_workflow_draft, validate_workflow_draft
|
from wf_artifacts.drafts import (
|
||||||
|
WorkflowDraft,
|
||||||
|
patch_workflow_draft,
|
||||||
|
validate_workflow_draft,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import WorkflowDraftWorkspace, summarize_draft_workspace
|
from .models import WorkflowDraftWorkspace, summarize_draft_workspace
|
||||||
from .store import DraftWorkspaceConflictError, DraftWorkspaceStore
|
from .store import DraftWorkspaceConflictError, DraftWorkspaceStore
|
||||||
@@ -22,11 +26,14 @@ def create_draft_workspace(
|
|||||||
"""Validate and save a new mutable draft workspace."""
|
"""Validate and save a new mutable draft workspace."""
|
||||||
now = _now_ms()
|
now = _now_ms()
|
||||||
validation = validate_workflow_draft(draft)
|
validation = validate_workflow_draft(draft)
|
||||||
|
normalized_draft = _canonical_draft_if_valid(
|
||||||
|
draft, validation_status=validation["status"]
|
||||||
|
)
|
||||||
workspace = WorkflowDraftWorkspace(
|
workspace = WorkflowDraftWorkspace(
|
||||||
id=workspace_id,
|
id=workspace_id,
|
||||||
revision=1,
|
revision=1,
|
||||||
title=title,
|
title=title,
|
||||||
draft=draft,
|
draft=normalized_draft,
|
||||||
status=validation["status"],
|
status=validation["status"],
|
||||||
diagnostics=validation["diagnostics"],
|
diagnostics=validation["diagnostics"],
|
||||||
created_at_epoch_ms=now,
|
created_at_epoch_ms=now,
|
||||||
@@ -66,7 +73,10 @@ def patch_draft_workspace(
|
|||||||
next_workspace = workspace.model_copy(
|
next_workspace = workspace.model_copy(
|
||||||
update={
|
update={
|
||||||
"revision": workspace.revision + 1,
|
"revision": workspace.revision + 1,
|
||||||
"draft": patched["draft"],
|
"draft": _canonical_draft_if_valid(
|
||||||
|
patched["draft"],
|
||||||
|
validation_status=patched["status"],
|
||||||
|
),
|
||||||
"status": patched["status"],
|
"status": patched["status"],
|
||||||
"diagnostics": patched["diagnostics"],
|
"diagnostics": patched["diagnostics"],
|
||||||
"updated_at_epoch_ms": _now_ms(),
|
"updated_at_epoch_ms": _now_ms(),
|
||||||
@@ -96,6 +106,23 @@ def _now_ms() -> int:
|
|||||||
return int(time.time() * 1000)
|
return int(time.time() * 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_draft_if_valid(
|
||||||
|
draft: JsonObject,
|
||||||
|
*,
|
||||||
|
validation_status: object,
|
||||||
|
) -> JsonObject:
|
||||||
|
"""Persist valid drafts in the canonical model shape.
|
||||||
|
|
||||||
|
Invalid drafts remain as-authored so diagnostics can still point at the
|
||||||
|
payload the client sent. Once validation passes, legacy `in`/`with`/`out`
|
||||||
|
maps become canonical `input`/`output` binding lists on disk and in MCP
|
||||||
|
responses.
|
||||||
|
"""
|
||||||
|
if validation_status != "valid":
|
||||||
|
return draft
|
||||||
|
return WorkflowDraft.model_validate(draft).model_dump(mode="json")
|
||||||
|
|
||||||
|
|
||||||
def _revision_conflict_payload(
|
def _revision_conflict_payload(
|
||||||
workspace: WorkflowDraftWorkspace,
|
workspace: WorkflowDraftWorkspace,
|
||||||
expected_revision: int,
|
expected_revision: int,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from wf_authoring import WorkflowBuilder
|
from wf_authoring import WorkflowBuilder
|
||||||
from wf_authoring.dsl import PathExpr
|
from wf_authoring.dsl import PathExpr
|
||||||
from wf_core import JoinNode, Workflow
|
from wf_core import JoinNode, Workflow
|
||||||
@@ -44,8 +42,8 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
|||||||
return builder.use_ref(
|
return builder.use_ref(
|
||||||
step.use,
|
step.use,
|
||||||
id=step_id,
|
id=step_id,
|
||||||
input=_draft_input_bindings(step),
|
input=step.input,
|
||||||
output=_draft_output_bindings(step),
|
output=step.output,
|
||||||
desc=step.desc,
|
desc=step.desc,
|
||||||
)
|
)
|
||||||
if isinstance(step, DraftForeachStep):
|
if isinstance(step, DraftForeachStep):
|
||||||
@@ -89,24 +87,3 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
|||||||
default=step.match.default,
|
default=step.match.default,
|
||||||
).entry
|
).entry
|
||||||
raise TypeError(f"unsupported draft step {type(step)!r}")
|
raise TypeError(f"unsupported draft step {type(step)!r}")
|
||||||
|
|
||||||
|
|
||||||
def _draft_input_bindings(step: DraftUseStep) -> list[dict[str, Any]]:
|
|
||||||
"""Translate draft input maps into canonical core input binding structs.
|
|
||||||
|
|
||||||
Draft JSON keeps `in` and `with` because they are compact patch targets for
|
|
||||||
LLM clients. The compiled workflow should not re-emit deprecated builder map
|
|
||||||
sugar, so this adapter boundary converts them to `NodeUse.input`.
|
|
||||||
"""
|
|
||||||
literal_bindings = [
|
|
||||||
{"target": target, "value": value} for target, value in step.with_.items()
|
|
||||||
]
|
|
||||||
path_bindings = [
|
|
||||||
{"target": target, "path": source} for source, target in step.in_.items()
|
|
||||||
]
|
|
||||||
return [*literal_bindings, *path_bindings]
|
|
||||||
|
|
||||||
|
|
||||||
def _draft_output_bindings(step: DraftUseStep) -> list[dict[str, str]]:
|
|
||||||
"""Translate draft output maps into canonical core output binding structs."""
|
|
||||||
return [{"source": source, "target": target} for source, target in step.out.items()]
|
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ def patch_workflow_draft(draft: JsonObject, patch: JsonPatch) -> JsonObject:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
result = validate_workflow_draft(patched)
|
result = validate_workflow_draft(patched)
|
||||||
|
if result["status"] == "valid":
|
||||||
|
patched = WorkflowDraft.model_validate(patched).model_dump(mode="json")
|
||||||
return {"draft": patched, **result}
|
return {"draft": patched, **result}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ from typing import Any, Literal
|
|||||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||||
|
|
||||||
from wf_core.models.conditions import Condition
|
from wf_core.models.conditions import Condition
|
||||||
|
from wf_core.models.steps import InputBinding, OutputBinding
|
||||||
|
|
||||||
JsonObject = dict[str, Any]
|
JsonObject = dict[str, Any]
|
||||||
STEP_KIND_KEYS = frozenset({
|
STEP_KIND_KEYS = frozenset(
|
||||||
|
{
|
||||||
"use",
|
"use",
|
||||||
"foreach",
|
"foreach",
|
||||||
"interrupt",
|
"interrupt",
|
||||||
@@ -15,7 +17,8 @@ STEP_KIND_KEYS = frozenset({
|
|||||||
"when",
|
"when",
|
||||||
"choose",
|
"choose",
|
||||||
"match",
|
"match",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DraftUseStep(BaseModel):
|
class DraftUseStep(BaseModel):
|
||||||
@@ -24,34 +27,67 @@ class DraftUseStep(BaseModel):
|
|||||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||||
|
|
||||||
use: str
|
use: str
|
||||||
in_: dict[str, str] = Field(
|
input: list[InputBinding] = Field(
|
||||||
default_factory=dict,
|
default_factory=list,
|
||||||
alias="in",
|
|
||||||
description=(
|
description=(
|
||||||
"Source-to-destination map from graph paths to node-local input "
|
"Canonical input bindings for this capability. Use path bindings "
|
||||||
"paths. Example: {'input.text': 'message'}. Values must be strings; "
|
"for graph-to-local input and value bindings for literals."
|
||||||
"use 'with' for literals."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
with_: dict[str, Any] = Field(
|
output: list[OutputBinding] = Field(
|
||||||
default_factory=dict,
|
default_factory=list,
|
||||||
alias="with",
|
|
||||||
description=(
|
description=(
|
||||||
"Static node-local input values keyed by destination input field/path. "
|
"Canonical output bindings from node-local output paths to workflow "
|
||||||
"Example: {'value': 'CLICKED'}."
|
"state destinations."
|
||||||
),
|
|
||||||
)
|
|
||||||
out: dict[str, str] = Field(
|
|
||||||
default_factory=dict,
|
|
||||||
description=(
|
|
||||||
"Source-to-destination map from node-local output paths to workflow "
|
|
||||||
"state destinations. Example: {'echoed': 'state.echoed'}."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
desc: str | None = None
|
desc: str | None = None
|
||||||
retry: int | None = Field(default=None, ge=0)
|
retry: int | None = Field(default=None, ge=0)
|
||||||
timeout_seconds: int | None = Field(default=None, gt=0)
|
timeout_seconds: int | None = Field(default=None, gt=0)
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _coerce_legacy_maps(cls, value: object) -> object:
|
||||||
|
"""Accept draft `in`/`with`/`out` maps as parse-only compatibility."""
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
|
||||||
|
data = dict(value)
|
||||||
|
legacy_input = data.pop("in", None)
|
||||||
|
legacy_with = data.pop("with", None)
|
||||||
|
legacy_output = data.pop("out", None)
|
||||||
|
if "input" in data and (legacy_input is not None or legacy_with is not None):
|
||||||
|
raise ValueError("cannot mix canonical input with legacy in/with maps")
|
||||||
|
if "output" in data and legacy_output is not None:
|
||||||
|
raise ValueError("cannot mix canonical output with legacy out map")
|
||||||
|
|
||||||
|
input_bindings = list(data.get("input", []))
|
||||||
|
output_bindings = list(data.get("output", []))
|
||||||
|
if legacy_with is not None:
|
||||||
|
if not isinstance(legacy_with, dict):
|
||||||
|
raise ValueError("draft use 'with' must be a mapping")
|
||||||
|
input_bindings.extend(
|
||||||
|
{"target": target, "value": literal}
|
||||||
|
for target, literal in legacy_with.items()
|
||||||
|
)
|
||||||
|
if legacy_input is not None:
|
||||||
|
if not isinstance(legacy_input, dict):
|
||||||
|
raise ValueError("draft use 'in' must be a mapping")
|
||||||
|
input_bindings.extend(
|
||||||
|
{"target": target, "path": source}
|
||||||
|
for source, target in legacy_input.items()
|
||||||
|
)
|
||||||
|
if legacy_output is not None:
|
||||||
|
if not isinstance(legacy_output, dict):
|
||||||
|
raise ValueError("draft use 'out' must be a mapping")
|
||||||
|
output_bindings.extend(
|
||||||
|
{"source": source, "target": target}
|
||||||
|
for source, target in legacy_output.items()
|
||||||
|
)
|
||||||
|
data["input"] = input_bindings
|
||||||
|
data["output"] = output_bindings
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class DraftForeachPayload(BaseModel):
|
class DraftForeachPayload(BaseModel):
|
||||||
"""Payload for one draft foreach step."""
|
"""Payload for one draft foreach step."""
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from wf_core.models.steps import (
|
|||||||
InputValueBinding,
|
InputValueBinding,
|
||||||
OutputBinding,
|
OutputBinding,
|
||||||
)
|
)
|
||||||
|
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
|
||||||
|
|
||||||
from ..events import make_event
|
from ..events import make_event
|
||||||
from ..models import RawWorkflowPlan
|
from ..models import RawWorkflowPlan
|
||||||
@@ -603,8 +604,8 @@ class WorkflowSurfaceHandlers:
|
|||||||
patch=[
|
patch=[
|
||||||
{
|
{
|
||||||
"op": "replace",
|
"op": "replace",
|
||||||
"path": f"/steps/{_escape_json_pointer(step_id)}/in",
|
"path": f"/steps/{_escape_json_pointer(step_id)}/input",
|
||||||
"value": input_map,
|
"value": _draft_input_bindings_payload(input_map, {}),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -623,8 +624,8 @@ class WorkflowSurfaceHandlers:
|
|||||||
patch=[
|
patch=[
|
||||||
{
|
{
|
||||||
"op": "replace",
|
"op": "replace",
|
||||||
"path": f"/steps/{_escape_json_pointer(step_id)}/out",
|
"path": f"/steps/{_escape_json_pointer(step_id)}/output",
|
||||||
"value": output_map,
|
"value": _draft_output_bindings_payload(output_map),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -657,9 +658,8 @@ class WorkflowSurfaceHandlers:
|
|||||||
steps: dict[str, Any] = {
|
steps: dict[str, Any] = {
|
||||||
DEFAULT_CALL_STEP_ID: {
|
DEFAULT_CALL_STEP_ID: {
|
||||||
"use": capability_name,
|
"use": capability_name,
|
||||||
"in": draft_input,
|
"input": _draft_input_bindings_payload(draft_input, draft_with),
|
||||||
"with": draft_with,
|
"output": _draft_output_bindings_payload(draft_output),
|
||||||
"out": draft_output,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
routes: dict[str, dict[str, str]] = {
|
routes: dict[str, dict[str, str]] = {
|
||||||
@@ -672,8 +672,13 @@ class WorkflowSurfaceHandlers:
|
|||||||
# exposes, a concrete state path that can become a runtime message.
|
# exposes, a concrete state path that can become a runtime message.
|
||||||
steps[DEFAULT_ERROR_STEP_ID] = {
|
steps[DEFAULT_ERROR_STEP_ID] = {
|
||||||
"use": RUNTIME_ERROR_CAPABILITY,
|
"use": RUNTIME_ERROR_CAPABILITY,
|
||||||
"in": {error_source: "message"},
|
"input": [
|
||||||
"out": {},
|
{
|
||||||
|
"target": {"root": "local", "parts": ["message"]},
|
||||||
|
"path": _graph_path_payload(error_source),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [],
|
||||||
}
|
}
|
||||||
routes[DEFAULT_CALL_STEP_ID][DEFAULT_ERROR_OUTCOME] = DEFAULT_ERROR_STEP_ID
|
routes[DEFAULT_CALL_STEP_ID][DEFAULT_ERROR_OUTCOME] = DEFAULT_ERROR_STEP_ID
|
||||||
routes[DEFAULT_ERROR_STEP_ID] = {DEFAULT_OK_OUTCOME: "__end__"}
|
routes[DEFAULT_ERROR_STEP_ID] = {DEFAULT_OK_OUTCOME: "__end__"}
|
||||||
@@ -1080,6 +1085,40 @@ 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 _graph_path_payload(value: str) -> dict[str, str | list[str]]:
|
||||||
|
return GraphSourcePath._serialize(GraphSourcePath.parse(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _local_path_payload(value: str) -> dict[str, str | list[str]]:
|
||||||
|
return LocalPath._serialize(LocalPath.parse(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _state_path_payload(value: str) -> dict[str, str | list[str]]:
|
||||||
|
return StatePath._serialize(StatePath.parse(value))
|
||||||
|
|
||||||
|
|
||||||
def _escape_json_pointer(value: str) -> str:
|
def _escape_json_pointer(value: str) -> str:
|
||||||
"""Escape one JSON Pointer path segment for generated JSON Patch helpers."""
|
"""Escape one JSON Pointer path segment for generated JSON Patch helpers."""
|
||||||
return value.replace("~", "~0").replace("/", "~1")
|
return value.replace("~", "~0").replace("/", "~1")
|
||||||
|
|||||||
@@ -44,8 +44,18 @@ def test_adapter_lowers_use_steps_to_canonical_bindings() -> None:
|
|||||||
"steps": {
|
"steps": {
|
||||||
"echo": {
|
"echo": {
|
||||||
"use": "demo.echo",
|
"use": "demo.echo",
|
||||||
"in": {"input.text": "text"},
|
"input": [
|
||||||
"out": {"echoed": "state.echoed"},
|
{
|
||||||
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
|
"path": {"root": "input", "parts": ["text"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {"echo": {"ok": "__end__"}},
|
"routes": {"echo": {"ok": "__end__"}},
|
||||||
@@ -76,8 +86,18 @@ def test_adapter_lowers_static_inputs_for_constant_like_steps() -> None:
|
|||||||
"steps": {
|
"steps": {
|
||||||
"constant": {
|
"constant": {
|
||||||
"use": "wf.std.constant",
|
"use": "wf.std.constant",
|
||||||
"with": {"value": "CLICKED"},
|
"input": [
|
||||||
"out": {"value": "state.message"},
|
{
|
||||||
|
"target": {"root": "local", "parts": ["value"]},
|
||||||
|
"value": "CLICKED",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["value"]},
|
||||||
|
"target": {"root": "state", "parts": ["message"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {"constant": {"ok": "__end__"}},
|
"routes": {"constant": {"ok": "__end__"}},
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ def test_patch_workflow_draft_uses_stable_step_paths() -> None:
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"op": "replace",
|
"op": "replace",
|
||||||
"path": "/steps/echo/in/input.text",
|
"path": "/steps/echo/input/0/target/parts/0",
|
||||||
"value": "message",
|
"value": "message",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["status"] == "valid"
|
assert result["status"] == "valid"
|
||||||
assert result["draft"]["steps"]["echo"]["in"]["input.text"] == "message"
|
assert result["draft"]["steps"]["echo"]["input"][0]["target"] == {
|
||||||
|
"root": "local",
|
||||||
|
"parts": ["message"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _keyed_echo_draft() -> dict[str, object]:
|
def _keyed_echo_draft() -> dict[str, object]:
|
||||||
@@ -37,8 +40,18 @@ def _keyed_echo_draft() -> dict[str, object]:
|
|||||||
"steps": {
|
"steps": {
|
||||||
"echo": {
|
"echo": {
|
||||||
"use": "demo.echo",
|
"use": "demo.echo",
|
||||||
"in": {"input.text": "text"},
|
"input": [
|
||||||
"out": {"echoed": "state.echoed"},
|
{
|
||||||
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
|
"path": {"root": "input", "parts": ["text"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {"echo": {"ok": "__end__"}},
|
"routes": {"echo": {"ok": "__end__"}},
|
||||||
|
|||||||
@@ -21,6 +21,40 @@ def test_workflow_draft_uses_keyed_steps() -> None:
|
|||||||
assert draft.steps["echo"].use == "demo.echo"
|
assert draft.steps["echo"].use == "demo.echo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_draft_accepts_legacy_use_maps_but_dumps_canonical_bindings() -> None:
|
||||||
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
|
**_keyed_echo_draft(),
|
||||||
|
"steps": {
|
||||||
|
"echo": {
|
||||||
|
"use": "demo.echo",
|
||||||
|
"in": {"input.text": "text"},
|
||||||
|
"with": {"limit": 3},
|
||||||
|
"out": {"echoed": "state.echoed"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dumped = draft.model_dump(mode="json")
|
||||||
|
|
||||||
|
assert "in" not in dumped["steps"]["echo"]
|
||||||
|
assert "with" not in dumped["steps"]["echo"]
|
||||||
|
assert "out" not in dumped["steps"]["echo"]
|
||||||
|
assert dumped["steps"]["echo"]["input"][0]["target"] == {
|
||||||
|
"root": "local",
|
||||||
|
"parts": ["limit"],
|
||||||
|
}
|
||||||
|
assert dumped["steps"]["echo"]["input"][1]["path"] == {
|
||||||
|
"root": "input",
|
||||||
|
"parts": ["text"],
|
||||||
|
}
|
||||||
|
assert dumped["steps"]["echo"]["output"][0]["target"] == {
|
||||||
|
"root": "state",
|
||||||
|
"parts": ["echoed"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_draft_step_requires_exactly_one_kind_key() -> None:
|
def test_draft_step_requires_exactly_one_kind_key() -> None:
|
||||||
draft = _keyed_echo_draft()
|
draft = _keyed_echo_draft()
|
||||||
steps = draft["steps"]
|
steps = draft["steps"]
|
||||||
@@ -36,7 +70,8 @@ def test_draft_step_requires_exactly_one_kind_key() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_workflow_draft_accepts_when_step() -> None:
|
def test_workflow_draft_accepts_when_step() -> None:
|
||||||
draft = WorkflowDraft.model_validate({
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
**_keyed_echo_draft(),
|
**_keyed_echo_draft(),
|
||||||
"start": "decide",
|
"start": "decide",
|
||||||
"steps": {
|
"steps": {
|
||||||
@@ -53,13 +88,15 @@ def test_workflow_draft_accepts_when_step() -> None:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
assert isinstance(draft.steps["decide"], DraftWhenStep)
|
assert isinstance(draft.steps["decide"], DraftWhenStep)
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_draft_accepts_choose_step() -> None:
|
def test_workflow_draft_accepts_choose_step() -> None:
|
||||||
draft = WorkflowDraft.model_validate({
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
**_keyed_echo_draft(),
|
**_keyed_echo_draft(),
|
||||||
"start": "choose_next",
|
"start": "choose_next",
|
||||||
"steps": {
|
"steps": {
|
||||||
@@ -79,13 +116,15 @@ def test_workflow_draft_accepts_choose_step() -> None:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
assert isinstance(draft.steps["choose_next"], DraftChooseStep)
|
assert isinstance(draft.steps["choose_next"], DraftChooseStep)
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_draft_accepts_match_step() -> None:
|
def test_workflow_draft_accepts_match_step() -> None:
|
||||||
draft = WorkflowDraft.model_validate({
|
draft = WorkflowDraft.model_validate(
|
||||||
|
{
|
||||||
**_keyed_echo_draft(),
|
**_keyed_echo_draft(),
|
||||||
"start": "match_status",
|
"start": "match_status",
|
||||||
"steps": {
|
"steps": {
|
||||||
@@ -101,7 +140,8 @@ def test_workflow_draft_accepts_match_step() -> None:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
assert isinstance(draft.steps["match_status"], DraftMatchStep)
|
assert isinstance(draft.steps["match_status"], DraftMatchStep)
|
||||||
|
|
||||||
@@ -116,8 +156,18 @@ def _keyed_echo_draft() -> dict[str, Any]:
|
|||||||
"steps": {
|
"steps": {
|
||||||
"echo": {
|
"echo": {
|
||||||
"use": "demo.echo",
|
"use": "demo.echo",
|
||||||
"in": {"input.text": "text"},
|
"input": [
|
||||||
"out": {"echoed": "state.echoed"},
|
{
|
||||||
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
|
"path": {"root": "input", "parts": ["text"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {"echo": {"ok": "__end__"}},
|
"routes": {"echo": {"ok": "__end__"}},
|
||||||
|
|||||||
@@ -277,8 +277,13 @@ def _draft() -> dict[str, Any]:
|
|||||||
"steps": {
|
"steps": {
|
||||||
"echo": {
|
"echo": {
|
||||||
"use": "demo.echo",
|
"use": "demo.echo",
|
||||||
"in": {},
|
"input": [],
|
||||||
"out": {"echoed": "state.echoed"},
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {"echo": {"ok": "__end__"}},
|
"routes": {"echo": {"ok": "__end__"}},
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ def test_workflow_surface_patches_draft_without_saving() -> None:
|
|||||||
patch=[
|
patch=[
|
||||||
{
|
{
|
||||||
"op": "replace",
|
"op": "replace",
|
||||||
"path": "/steps/echo/in/input.text",
|
"path": "/steps/echo/input/0/target/parts/0",
|
||||||
"value": "message",
|
"value": "message",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -443,7 +443,10 @@ def test_workflow_surface_patches_draft_without_saving() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert payload["status"] == "valid"
|
assert payload["status"] == "valid"
|
||||||
assert payload["draft"]["steps"]["echo"]["in"]["input.text"] == "message"
|
assert payload["draft"]["steps"]["echo"]["input"][0]["target"] == {
|
||||||
|
"root": "local",
|
||||||
|
"parts": ["message"],
|
||||||
|
}
|
||||||
assert not artifact_store.list_artifacts()
|
assert not artifact_store.list_artifacts()
|
||||||
|
|
||||||
|
|
||||||
@@ -579,8 +582,18 @@ def test_workflow_surface_patch_helpers_update_draft_workspace() -> None:
|
|||||||
assert output_mapped["revision"] == 5
|
assert output_mapped["revision"] == 5
|
||||||
assert fetched["draft"]["name"] == "echo_v2"
|
assert fetched["draft"]["name"] == "echo_v2"
|
||||||
assert fetched["draft"]["routes"]["echo"]["error"] == "__end__"
|
assert fetched["draft"]["routes"]["echo"]["error"] == "__end__"
|
||||||
assert fetched["draft"]["steps"]["echo"]["in"] == {"input.text": "message"}
|
assert fetched["draft"]["steps"]["echo"]["input"] == [
|
||||||
assert fetched["draft"]["steps"]["echo"]["out"] == {"echoed": "state.echoed"}
|
{
|
||||||
|
"target": {"root": "local", "parts": ["message"]},
|
||||||
|
"path": {"root": "input", "parts": ["text"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert fetched["draft"]["steps"]["echo"]["output"] == [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_validates_draft_workspace_with_live_outcomes() -> None:
|
def test_workflow_surface_validates_draft_workspace_with_live_outcomes() -> None:
|
||||||
@@ -654,7 +667,7 @@ def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() ->
|
|||||||
|
|
||||||
result = asyncio.run(
|
result = asyncio.run(
|
||||||
handlers.create_minimal_draft_workspace(
|
handlers.create_minimal_draft_workspace(
|
||||||
workspace_id="echo_draft",
|
workspace_id="echo_draft_canonical_error",
|
||||||
name="echo",
|
name="echo",
|
||||||
capability_name="demo.personal.mcp_echo_tool",
|
capability_name="demo.personal.mcp_echo_tool",
|
||||||
input_schema={
|
input_schema={
|
||||||
@@ -673,13 +686,20 @@ def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() ->
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert service.draft_workspace_store is not None
|
assert service.draft_workspace_store is not None
|
||||||
workspace = service.draft_workspace_store.get_workspace("echo_draft")
|
workspace = service.draft_workspace_store.get_workspace(
|
||||||
|
"echo_draft_canonical_error"
|
||||||
|
)
|
||||||
|
|
||||||
assert result["workspace_id"] == "echo_draft"
|
assert result["workspace_id"] == "echo_draft_canonical_error"
|
||||||
assert workspace.draft["routes"]["call"]["ok"] == "__end__"
|
assert workspace.draft["routes"]["call"]["ok"] == "__end__"
|
||||||
assert workspace.draft["routes"]["call"]["error"] == "tool_error"
|
assert workspace.draft["routes"]["call"]["error"] == "tool_error"
|
||||||
assert workspace.draft["steps"]["tool_error"]["use"] == "wf.std.runtime_error"
|
assert workspace.draft["steps"]["tool_error"]["use"] == "wf.std.runtime_error"
|
||||||
assert workspace.draft["steps"]["tool_error"]["in"] == {"state.echoed": "message"}
|
assert workspace.draft["steps"]["tool_error"]["input"] == [
|
||||||
|
{
|
||||||
|
"target": {"root": "local", "parts": ["message"]},
|
||||||
|
"path": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace() -> None:
|
def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace() -> None:
|
||||||
@@ -693,7 +713,7 @@ def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace() ->
|
|||||||
|
|
||||||
result = asyncio.run(
|
result = asyncio.run(
|
||||||
handlers.create_minimal_draft_workspace(
|
handlers.create_minimal_draft_workspace(
|
||||||
workspace_id="echo_draft",
|
workspace_id="echo_draft_canonical",
|
||||||
name="echo",
|
name="echo",
|
||||||
capability_name="demo.personal.echo_tool",
|
capability_name="demo.personal.echo_tool",
|
||||||
input_schema={"type": "object"},
|
input_schema={"type": "object"},
|
||||||
@@ -714,11 +734,21 @@ def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace() ->
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert service.draft_workspace_store is not None
|
assert service.draft_workspace_store is not None
|
||||||
workspace = service.draft_workspace_store.get_workspace("echo_draft")
|
workspace = service.draft_workspace_store.get_workspace("echo_draft_canonical")
|
||||||
|
|
||||||
assert result["workspace_id"] == "echo_draft"
|
assert result["workspace_id"] == "echo_draft_canonical"
|
||||||
assert workspace.draft["steps"]["call"]["in"] == {"input.text": "text"}
|
assert workspace.draft["steps"]["call"]["input"] == [
|
||||||
assert workspace.draft["steps"]["call"]["out"] == {"echoed": "state.echoed"}
|
{
|
||||||
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
|
"path": {"root": "input", "parts": ["text"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert workspace.draft["steps"]["call"]["output"] == [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> None:
|
def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> None:
|
||||||
@@ -737,20 +767,32 @@ def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> Non
|
|||||||
|
|
||||||
result = asyncio.run(
|
result = asyncio.run(
|
||||||
handlers.create_draft_workspace_from_capability(
|
handlers.create_draft_workspace_from_capability(
|
||||||
workspace_id="echo_from_capability",
|
workspace_id="echo_from_capability_canonical",
|
||||||
capability_name="demo.personal.echo_tool",
|
capability_name="demo.personal.echo_tool",
|
||||||
name="echo_from_capability",
|
name="echo_from_capability",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert service.draft_workspace_store is not None
|
assert service.draft_workspace_store is not None
|
||||||
workspace = service.draft_workspace_store.get_workspace("echo_from_capability")
|
workspace = service.draft_workspace_store.get_workspace(
|
||||||
|
"echo_from_capability_canonical"
|
||||||
|
)
|
||||||
|
|
||||||
assert result["workspace_id"] == "echo_from_capability"
|
assert result["workspace_id"] == "echo_from_capability_canonical"
|
||||||
assert result["wrapper_hints"]["input_map"] == {"input.text": "text"}
|
assert result["wrapper_hints"]["input_map"] == {"input.text": "text"}
|
||||||
assert result["wrapper_hints"]["output_map"] == {"echoed": "state.echoed"}
|
assert result["wrapper_hints"]["output_map"] == {"echoed": "state.echoed"}
|
||||||
assert workspace.draft["steps"]["call"]["use"] == "demo.personal.echo_tool"
|
assert workspace.draft["steps"]["call"]["use"] == "demo.personal.echo_tool"
|
||||||
assert workspace.draft["steps"]["call"]["in"] == {"input.text": "text"}
|
assert workspace.draft["steps"]["call"]["input"] == [
|
||||||
assert workspace.draft["steps"]["call"]["out"] == {"echoed": "state.echoed"}
|
{
|
||||||
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
|
"path": {"root": "input", "parts": ["text"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert workspace.draft["steps"]["call"]["output"] == [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_creates_artifact_from_workspace() -> None:
|
def test_workflow_surface_creates_artifact_from_workspace() -> None:
|
||||||
@@ -1262,8 +1304,18 @@ def _echo_draft() -> dict[str, Any]:
|
|||||||
"steps": {
|
"steps": {
|
||||||
"echo": {
|
"echo": {
|
||||||
"use": "demo.personal.echo_tool",
|
"use": "demo.personal.echo_tool",
|
||||||
"in": {"input.text": "text"},
|
"input": [
|
||||||
"out": {"echoed": "state.echoed"},
|
{
|
||||||
|
"target": {"root": "local", "parts": ["text"]},
|
||||||
|
"path": {"root": "input", "parts": ["text"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["echoed"]},
|
||||||
|
"target": {"root": "state", "parts": ["echoed"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routes": {"echo": {"ok": "__end__"}},
|
"routes": {"echo": {"ok": "__end__"}},
|
||||||
|
|||||||
Reference in New Issue
Block a user