nodus-workflow-ai
Run a generated plan as a Nodus graph: validated as a whole before any step runs, under a capability grant narrowed to what the plan declared.
Design record: docs/design/v5/07-generated-plans.md in the
nodus-lang repo. Issue:
#93.
What this is not
- Not a planner.
nodus-agenthas two (LocalPlanner,LLMPlanner). - Not an executor.
task/graph/run_graphalready run a graph built from data at runtime. - Not a loop.
goal … over … untilis the loop.
The execution substrate already existed. What was missing was the bridge from a planner's flat list of steps to a DAG, and a contract for what is checked before a generated plan is allowed to run.
Usage
from nodus import NodusRuntime
from nodus_agent import LocalPlanner
from nodus_workflow_ai import validate_plan, run_plan, PlanRejected
raw = LocalPlanner().plan("fetch then analyze", tools, context)
try:
plan = validate_plan(raw, available_tools=[t["name"] for t in tools])
except PlanRejected as rejected:
for finding in rejected.findings:
print("rejected:", finding)
else:
outcome = run_plan(plan, NodusRuntime(timeout_ms=None))
print(outcome["result"]["steps"]) # {"fetch": ..., "analyze": ...}
print(outcome["parallel_groups"]) # [["fetch"], ["analyze"]]
The two things that carry the weight
Validation happens before anything runs
A declared workflow is checked by the compiler. A generated one has no compiler, so the checks have to be somewhere, and "the model was careful" is not somewhere.
A plan that will fail at step 7 because a tool does not exist is rejected at step 0. The run has not touched the world yet; after step 1 it has.
Checked: every named tool exists, every tool is permitted (when the host says
what it permits), the plan is under its step ceiling, names are unique, and every
after names a step that is actually in the plan.
Not checked, deliberately: acyclicity and dependency resolution, which come free from construction — a dependency is a task value, so you cannot reference one you do not have, and closing a cycle would need a forward reference. A second implementation of a guarantee the substrate already gives is the one that drifts.
The grant is narrowed to the plan
A generated plan runs under the tools it declared, not the host's ambient policy. The plan names its tools before it runs; that list is exactly the grant it needs. A plan attempting a tool it did not declare is refused — not because the tool is forbidden in general, but because this plan did not ask for it.
That is a real strengthening over "run the plan with the host's policy", and it costs nothing to compute: the tool list is already being collected for the existence check.
Dependency edges
A planner emits a flat list; a workflow is a DAG. Something has to decide the edges.
| the planner said | this step depends on |
|---|---|
"after": ["fetch"] |
exactly that |
"after": [] |
nothing — a declared root |
| nothing at all | the step before it |
An explicit empty list and an absent key are different statements. Collapsing them would make a planner unable to declare parallelism without also annotating every step it forgot.
The fallback is per step, not per plan: a plan may annotate some steps and not others, and falling back wholesale because one step was silent would discard the edges the planner did emit.
Edges are not inferred from data flow. That is where this wants to end up,
and it needs a plan format declaring each step's inputs and outputs, which
PlannerBackend does not have — a change to a published package's protocol, and
a second decision rather than a prerequisite.
What this does not promise
A generated plan's goal is not statically satisfiable. For a declared goal,
reached("label") is checked against checkpoint literals in the source. A
generated plan has no literals — the predicate and the steps both come from the
model, so checking one against the other proves only that the model was
self-consistent. Synthesising labels to make the existing check pass would give a
check that cannot fail, which reads as a guarantee and is worse than an absent
one. The budget bounds the loop; nothing else does.
A generated run is not resumable across processes. A declared workflow rebuilds from source on resume; a generated graph was built from a value that existed in one process. This is the one place a generated workflow is genuinely weaker than a declared one, and it should not be discovered by a user.
A wrong edge is a real failure mode. A model asserting after: ["analyze"]
on a step that does not need it costs parallelism; asserting the reverse costs
correctness. Validation bounds what a plan can name; it cannot judge whether an
edge is right.
Requirements
nodus-lang >= 5.8.0 — step names on task() landed in
#679, and without them a
generated run reports an empty steps map.
nodus-agent is optional, and only for its planners.
Tests
pytest tests/ -q
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nodus_workflow_ai-0.1.0.tar.gz.
File metadata
- Download URL: nodus_workflow_ai-0.1.0.tar.gz
- Upload date:
- Size: 14.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14a2536b17f3def736e5ea26ce1435932e5218fcc14f3dc71c9bd8bf01d9f958
|
|
| MD5 |
1f7c90af26f98cc4edf72511f1498161
|
|
| BLAKE2b-256 |
1a07bda313ed0c8a12cab1f20f32763f7dbc6843706f83edcf07382de7159a70
|
File details
Details for the file nodus_workflow_ai-0.1.0-py3-none-any.whl.
File metadata
- Download URL: nodus_workflow_ai-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
39db70ac1b98e08772be60a667a017970e542fcda3019757ad731f2faa2e2a43
|
|
| MD5 |
c810fa409a762d5d95af8f38c781c998
|
|
| BLAKE2b-256 |
d6d6b796f3eb9ee9ef7abb9919ba5ea975d7dd7960a5a0e42175fced361db2fb
|