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
|
If sync runtime encounters an async node, fail clearly rather than faking a sync
|
||||||
bridge.
|
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
|
## Future extension
|
||||||
|
|
||||||
### `Workflow -> NodeSpec`
|
### `Workflow -> NodeSpec`
|
||||||
|
|||||||
+5
-5
@@ -5,14 +5,14 @@ description = "Add your description here"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mcp[cli,rich]>=1.27.0",
|
"mcp[cli,rich]>=1",
|
||||||
"pydantic>=2.13.3",
|
"pydantic>=2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.4.0",
|
"pytest>=8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
# [tool.pytest.ini_options]
|
||||||
addopts = "-p no:cacheprovider"
|
# addopts = "-p no:cacheprovider"
|
||||||
|
|||||||
+9
-80
@@ -1,88 +1,17 @@
|
|||||||
# Graph as struct? workflow as struct
|
# 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
|
The cleaned spec starts below.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -264,12 +264,12 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1.27.0" },
|
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1" },
|
||||||
{ name = "pydantic", specifier = ">=2.13.3" },
|
{ name = "pydantic", specifier = ">=2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "pytest", specifier = ">=8.4.0" }]
|
dev = [{ name = "pytest", specifier = ">=8" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ class PathExpr:
|
|||||||
def lt(self, other: object) -> Expr:
|
def lt(self, other: object) -> Expr:
|
||||||
return self._binary("lt", other)
|
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)
|
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)
|
return self._binary("ne", other)
|
||||||
|
|
||||||
def __gt__(self, other: object) -> Expr:
|
def __gt__(self, other: object) -> Expr:
|
||||||
|
|||||||
Reference in New Issue
Block a user