docs: show Python client workflow

This commit is contained in:
lda
2026-08-30 23:17:11 +07:00 Verified
parent 23f2681b28
commit 49b62dc3a5
@@ -36,6 +36,166 @@ existing workflow API surface, HTTP JSON-RPC adapter, domain models, and
- Capability nodes and native subgraphs remain distinct authoring operations. - Capability nodes and native subgraphs remain distinct authoring operations.
- Draft workspaces do not appear in the public `wf_client` interface. - Draft workspaces do not appear in the public `wf_client` interface.
## Hypothetical Usage
This section is the intended experience. Names may tighten during
implementation, but the lifecycle and amount of required ceremony are part of
the design contract.
### Connect and use one capability
```python
from wf_client import App
app = App.from_http_jsonrpc("http://localhost:8765/rpc")
search = await app.capability("app.default.search")
print(search.input_schema)
result = await search(query="durable workflow systems")
print(result.outcome)
print(result.output)
```
`search` is a validated Python object, not an RPC response dictionary. Calling
it validates input locally, uses the workflow capability operation, validates
the response, and returns a `CapabilityResult`.
### Build, save, and run a workflow
```python
from pydantic import BaseModel, Field
from wf_authoring import input_from, input_path, output_to, state_path
from wf_client import App
class ReportInput(BaseModel):
topic: str
class ReportState(BaseModel):
sources: list[str] = Field(default_factory=list)
class ReportOutput(BaseModel):
sources: list[str]
app = App.from_http_jsonrpc("http://localhost:8765/rpc")
search = await app.capability("app.default.search")
graph = app.new_workflow(
"report",
input_schema=ReportInput,
state_schema=ReportState,
output_schema=ReportOutput,
)
searched = graph.use(
search,
id="search",
input=[input_from(input_path("topic"), "query")],
output=[output_to("sources", state_path("sources"))],
)
done = graph.end("ok", id="end_ok")
graph.set_entry_point(searched)
graph.connect(searched, "ok", done)
graph.set_output([input_from(state_path("sources"), "sources")])
report_v1 = await graph.save(version=1, title="Research report")
run = await report_v1.run({"topic": "durable workflow systems"})
print(run.status)
print(run.output)
```
`graph` is an `EditableWorkflow`, which subclasses `WorkflowBuilder`. Existing
builder operations such as `use()`, `when()`, `choose()`, `match()`,
`foreach()`, `interrupt()`, `end()`, `connect()`, and `set_entry_point()` remain
available directly. `wf_client` adds remote-capability use, provenance,
lossless workflow output bindings, and `save()`.
The convenient `artifact.run()` form is available only when source bindings
are already unambiguous. It never chooses between accounts.
### Bind an environment explicitly
```python
production = await report_v1.deploy(
"report.production",
bindings={
"app.default": "company.production",
},
)
validation = await production.validate()
if not validation.runnable:
for diagnostic in validation.diagnostics:
print(diagnostic.message)
else:
run = await production.run({"topic": "durable workflow systems"})
```
Artifacts describe logical source requirements. Deployments select the actual
configured sources for an environment. Built-in platform-only workflows need
little or no deployment ceremony.
### Edit an immutable saved version
```python
report_v2_graph = await app.edit_workflow("report", version=1)
summarize = await app.capability("app.default.summarize")
summarized = report_v2_graph.use(summarize, id="summarize")
report_v2_graph.set_route("search", "ok", summarized)
report_v2_graph.connect(summarized, "ok", "end_ok")
report_v2 = await report_v2_graph.save(version=2)
```
Version 1 remains unchanged. `edit_workflow()` validates the artifact and its
plan, then seeds the same builder methods with every schema, node, edge,
binding, outcome, and subgraph reference preserved.
### Reuse a saved workflow as a native subgraph
```python
parent = app.new_workflow(
"weekly_digest",
input_schema=DigestInput,
state_schema=DigestState,
output_schema=DigestOutput,
)
report_step = parent.subgraph(
report_v2,
input=[input_from(input_path("topic"), "topic")],
output=[output_to("sources", state_path("report_sources"))],
)
```
An ordinary remote capability uses `graph.use(capability)`. An immutable saved
workflow uses `graph.subgraph(artifact)`. The distinction remains visible
because their dependency and runtime semantics differ.
### Resume an interrupted run
```python
run = await report_v2.run({"topic": "durable workflow systems"})
if run.interrupt is not None:
print(run.interrupt.request)
run = await run.resume({"approved": True})
for frame in (await run.trace(limit=25)).frames:
print(frame.node_id, frame.outcome)
```
Rich representations may summarize these objects in IPython, but displaying an
object never performs I/O or executes a capability.
## Public Package ## Public Package
```text ```text
@@ -195,7 +355,7 @@ The authoring adapter consumes its reference and schema contract directly.
### Editable workflow ### Editable workflow
```python ```python
class EditableWorkflow: class EditableWorkflow(WorkflowBuilder):
name: str name: str
based_on: ArtifactRef | None based_on: ArtifactRef | None
@@ -222,6 +382,16 @@ class EditableWorkflow:
def set_entry_point(self, step: StepRef) -> None: ... def set_entry_point(self, step: StepRef) -> None: ...
def connect(self, source: StepRef, outcome: str, target: StepRef) -> None: ... def connect(self, source: StepRef, outcome: str, target: StepRef) -> None: ...
def set_route(
self,
source: StepRef,
outcome: str,
target: StepRef,
) -> None: ...
def remove_route(self, source: StepRef, outcome: str) -> None: ...
def remove_step(self, step: StepRef) -> None: ...
def set_output( def set_output(
self, self,
bindings: Sequence[StepInputBindingArg], bindings: Sequence[StepInputBindingArg],
@@ -240,9 +410,19 @@ class EditableWorkflow:
) -> WorkflowArtifact: ... ) -> WorkflowArtifact: ...
``` ```
`EditableWorkflow` uses composition around `WorkflowBuilder`. It delegates `EditableWorkflow` subclasses `WorkflowBuilder` from the separate `wf_client`
ordinary local authoring methods and owns only remote-capability adaptation, package. `WorkflowBuilder` stays transport-free, while the subclass inherits
artifact provenance, and saving through the connected client port. its full authoring interface and adds remote-capability adaptation, artifact
provenance, and saving through the connected client port. Do not copy builder
methods into a parallel wrapper and do not use dynamic `__getattr__`
delegation; both approaches would create typing and drift problems.
Seeded editing also requires focused `set_route()`, `remove_route()`, and
`remove_step()` mutations. `set_route()` replaces the unique edge for one
source/outcome pair rather than appending a duplicate. `remove_step()` rejects
removal while routes still reference the step, so callers must deliberately
rewire or remove those routes first. These are local builder operations, not
draft API calls.
`set_output()` must first become a lossless `WorkflowBuilder` operation. `set_output()` must first become a lossless `WorkflowBuilder` operation.
`compile()` must preserve workflow output bindings. This repair is required `compile()` must preserve workflow output bindings. This repair is required