documents: async? mcp? schema type validation?
This commit is contained in:
@@ -124,6 +124,101 @@ Preferred design:
|
||||
If sync runtime encounters an async node, fail clearly rather than faking a sync
|
||||
bridge.
|
||||
|
||||
## Async runtime seam
|
||||
|
||||
The intended async work should be additive, not a rewrite.
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- keep current sync runtime as the stable baseline
|
||||
- add async siblings instead of mutating the sync path into a mixed mode
|
||||
- avoid hidden sync-to-async or async-to-sync bridges in core execution
|
||||
|
||||
Likely async entry points:
|
||||
|
||||
- `execute_workflow_async(...)`
|
||||
- `resume_workflow_async(...)`
|
||||
- `step_workflow_async(...)`
|
||||
- `execute_node_use_async(...)`
|
||||
|
||||
Likely registry split:
|
||||
|
||||
- sync registry: `dict[str, SyncNodeHandler]`
|
||||
- async registry: `dict[str, AsyncNodeHandler]`
|
||||
|
||||
`NodeSpec` should support both export paths:
|
||||
|
||||
- `to_registry_handler()` for sync callables only
|
||||
- `to_async_registry_handler()` for sync or async callables
|
||||
- `build_registry(...)` for sync specs
|
||||
- `build_async_registry(...)` for mixed or async specs
|
||||
|
||||
This keeps the rules simple:
|
||||
|
||||
- sync runtime executes sync handlers only
|
||||
- async runtime can execute both sync and async handlers
|
||||
- async runtime is the natural home for future MCP-backed tool nodes
|
||||
|
||||
## MCP proxy layer
|
||||
|
||||
MCP integration should sit above `wf_core`, not inside it.
|
||||
|
||||
Recommended layering:
|
||||
|
||||
1. MCP client or proxy code discovers tools
|
||||
2. each MCP tool is wrapped as a `NodeSpec`
|
||||
3. wrapped specs enter the same `NodeCatalog` as handwritten nodes
|
||||
4. the client LLM builds graphs against one unified catalog
|
||||
5. workflows compile to the existing core `Workflow`
|
||||
6. runtime executes registry handlers without caring whether the backing tool is local Python or MCP
|
||||
|
||||
This means MCP tools should look like ordinary nodes at the authoring boundary:
|
||||
|
||||
- declared input model
|
||||
- declared output model
|
||||
- declared outcomes
|
||||
- description/docs for LLM consumption
|
||||
- sync or async execution capability
|
||||
|
||||
The proxy/MCP layer should be responsible for:
|
||||
|
||||
- tool discovery
|
||||
- schema translation
|
||||
- auth/session concerns
|
||||
- wrapping tool calls into `NodeSpec`s
|
||||
|
||||
The core runtime should remain responsible only for:
|
||||
|
||||
- mapping input/state/context into node payloads
|
||||
- validating payloads
|
||||
- routing outcomes
|
||||
- writing mapped output into workflow state
|
||||
- interrupts, frames, trace, and foreach semantics
|
||||
|
||||
## Type validation stance
|
||||
|
||||
Current core validation is intentionally shallow. Today it mainly enforces:
|
||||
|
||||
- object payloads are dict-like
|
||||
- required keys exist
|
||||
|
||||
It does not yet fully enforce:
|
||||
|
||||
- scalar field types
|
||||
- nested object structure
|
||||
- array item types
|
||||
- enums/literals
|
||||
|
||||
Near-term recommendation:
|
||||
|
||||
- keep `SchemaRef` as the portable contract/export shape
|
||||
- keep using `pydantic.BaseModel` as the strongest validation layer for authored nodes
|
||||
- gradually strengthen core schema validation where it pays off
|
||||
|
||||
This is especially useful for MCP wrapping, because the proxy layer can often
|
||||
normalize a tool contract into Pydantic models before the workflow runtime sees
|
||||
it.
|
||||
|
||||
## Future extension
|
||||
|
||||
### `Workflow -> NodeSpec`
|
||||
|
||||
+5
-5
@@ -5,14 +5,14 @@ description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"mcp[cli,rich]>=1.27.0",
|
||||
"pydantic>=2.13.3",
|
||||
"mcp[cli,rich]>=1",
|
||||
"pydantic>=2",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.4.0",
|
||||
"pytest>=8",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-p no:cacheprovider"
|
||||
# [tool.pytest.ini_options]
|
||||
# addopts = "-p no:cacheprovider"
|
||||
|
||||
+9
-80
@@ -1,88 +1,17 @@
|
||||
# Graph as struct? workflow as struct
|
||||
|
||||
One graph is one input output
|
||||
This file is now mostly a working design note.
|
||||
|
||||
i decide to make edge dumb as shit. Nodes do all the work
|
||||
The original top section was rough brainstorming around a few ideas that did end
|
||||
up surviving:
|
||||
|
||||
struct needs to define in, out and ALL states. any output from a node is either writing to state or writing to a key of it.
|
||||
- edges should stay dumb
|
||||
- nodes should own business outcomes
|
||||
- graph use-sites should explicitly map input and output
|
||||
- workflow state needs declared merge behavior
|
||||
- a workflow/subgraph should eventually be reusable like a function
|
||||
|
||||
metadata for states to hide in tracing
|
||||
|
||||
whatever language works
|
||||
|
||||
## plan
|
||||
|
||||
dumbass stupid plan fuhh ahh plan
|
||||
|
||||
```text
|
||||
.in -> pydantic.create_model
|
||||
.out -> pydantic.create_model
|
||||
.state // langgraph partial update style
|
||||
.steps[] // either nodes only or all everything (all everything seems cooler I LIKE ENUMS but it could be stupih)
|
||||
.steps[]:
|
||||
.type IN node, edge
|
||||
node:
|
||||
.tool // ours, so do ts well
|
||||
.args
|
||||
.desc?
|
||||
.bind? // either update .state or .state[.bind]
|
||||
edge:
|
||||
.from
|
||||
.to
|
||||
```
|
||||
|
||||
### next node
|
||||
|
||||
GENUINELY HOW do i convey next node?
|
||||
do i pass next node in arg? args.next.{this cond, that cond}
|
||||
|
||||
function do need to know about where its run. Who called. So it can decide where it goes next. how? idk maybe a context struct that has the full next list from .edges[]?
|
||||
|
||||
or can it just reference any next node? but how? who tells it what node?
|
||||
|
||||
function 100% can select what node it wants to go next if supported. Since this is MY stuff, a function I make can do that.
|
||||
|
||||
### registry
|
||||
|
||||
past graph can turn into a function. so a graph is lowk same thing as a fn.
|
||||
|
||||
### input
|
||||
|
||||
how in the FUCK do i convey input!
|
||||
|
||||
this is how:
|
||||
|
||||
since input is state, this function kinda doesnt care about args. Args comes from state.
|
||||
|
||||
a function cares about certain keys (args) looking this shape, the least i can do is a remap on node {"specialized graph state key": "generic fn input key"}
|
||||
|
||||
same as output {"generic fn answer key": "specialized graph state key"}
|
||||
|
||||
### this is lowk constricted
|
||||
|
||||
But the counterpart is freeform code...
|
||||
|
||||
### more problems
|
||||
|
||||
batch? foreach?
|
||||
|
||||
control flow.
|
||||
|
||||
maybe special nodes that does control flow. Like ensure: something that only after all nodes connected to it ran does it run the nodes it connects to?
|
||||
|
||||
if bool = exist, maybe truth table node... if .state.key1 and .state.key2
|
||||
|
||||
## Alternative plan! god damn
|
||||
|
||||
that was looking like langgraphs Graph API, we could have Functional API being a taddddd simpler: functions imported and run in a sealed ahh box.
|
||||
|
||||
functional api is actually peam. its like async.
|
||||
|
||||
## inspo
|
||||
|
||||
my little use of langgraph
|
||||
my nonexistent knowledge of openai fn schema
|
||||
claude
|
||||
The cleaned spec starts below.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -264,12 +264,12 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1.27.0" },
|
||||
{ name = "pydantic", specifier = ">=2.13.3" },
|
||||
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1" },
|
||||
{ name = "pydantic", specifier = ">=2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = ">=8.4.0" }]
|
||||
dev = [{ name = "pytest", specifier = ">=8" }]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
|
||||
@@ -75,10 +75,10 @@ class PathExpr:
|
||||
def lt(self, other: object) -> Expr:
|
||||
return self._binary("lt", other)
|
||||
|
||||
def __eq__(self, other: object) -> Expr: # ty: ignore[invalid-method-override]
|
||||
def __eq__(self, other: object) -> Expr: # type: ignore[override] # ty: ignore[invalid-method-override]
|
||||
return self._binary("eq", other)
|
||||
|
||||
def __ne__(self, other: object) -> Expr: # ty: ignore[invalid-method-override]
|
||||
def __ne__(self, other: object) -> Expr: # type: ignore[override] # ty: ignore[invalid-method-override]
|
||||
return self._binary("ne", other)
|
||||
|
||||
def __gt__(self, other: object) -> Expr:
|
||||
|
||||
Reference in New Issue
Block a user