Skip to main content

A governed dynamic-graph runtime: a stable Supervisor plans, validates, and runs transient LangGraph workflows from a bounded node registry.

Project description

Dynamic Subgraphs

License: Apache 2.0 Python 3.11+ CI Typed

A governed dynamic graph runtime: a stable Supervisor plans, validates, and runs transient LangGraph workflows assembled from a bounded node registry and a validated GraphSpec, recording every run under runs/<run_id>/. The planner emits plans, never executable code; the compiler only instantiates registry-approved node kinds; and LangGraph types stay behind the compiler/ and runtime/ boundaries. An optional thin FastAPI layer exposes the supervisor over HTTP.

What can it build?

From one prompt, the planner assembles a transient graph out of a small set of governed node kinds — llm_call, tool_call, reduce, parallel_map, spawn_subagent, spawn_subgraph, wait_for_event, emit_artifact. A few of the shapes it produces (these render in the same Mermaid the engine writes to graph.mmd for every run):

Parallel research → recommend — fan out to independent workers, then reduce:

graph TD
    START([START])
    END([END])
    extract_a["extract_a<br/>tool_call"]
    extract_b["extract_b<br/>tool_call"]
    summarize_a["summarize_a<br/>llm_call"]
    summarize_b["summarize_b<br/>llm_call"]
    compare["compare_and_recommend<br/>reduce"]
    START --> extract_a
    START --> extract_b
    extract_a --> summarize_a
    extract_b --> summarize_b
    summarize_a --> compare
    summarize_b --> compare
    compare --> END

Tool-grounded answer — search the web, answer from it, write a report:

graph TD
    START([START])
    END([END])
    search["web_search<br/>tool_call"]
    answer["answer<br/>llm_call"]
    report["report<br/>emit_artifact"]
    START --> search
    search --> answer
    answer --> report
    report --> END

Human-in-the-loop — pause for an external event, then resume:

graph TD
    START([START])
    END([END])
    draft["draft_proposal<br/>llm_call"]
    review["await_approval<br/>wait_for_event"]
    finalize["finalize<br/>llm_call"]
    START --> draft
    draft --> review
    review --> finalize
    finalize --> END

Nested composition — a node plans and runs a bounded child graph:

graph TD
    START([START])
    END([END])
    plan["plan<br/>llm_call"]
    investigate["investigate<br/>spawn_subgraph"]
    synthesize["synthesize<br/>reduce"]
    START --> plan
    plan --> investigate
    investigate --> synthesize
    synthesize --> END

How it works

Those graphs are transient — planned, validated, run, and recorded per request. The only thing that stays fixed is the Supervisor, a stable host graph that governs every run:

graph LR
    START([prompt]) --> plan
    plan["plan<br/>(GraphSpec)"] --> validate
    validate["validate<br/>(registry)"] --> run
    run["compile & run<br/>(transient graph)"] --> record
    record["record<br/>(runs/&lt;id&gt;/)"] --> respond
    respond([result]) --> END([END])

The planner emits a plan, never code; the compiler only instantiates registry-approved node kinds; and every run — success or failure — is recorded.

Install

With uv (recommended):

uv add dynamic-subgraphs                 # slim core (engine only)
uv add "dynamic-subgraphs[openai]"       # + OpenAI provider
uv add "dynamic-subgraphs[anthropic]"    # + Anthropic provider
uv add "dynamic-subgraphs[ollama]"       # + local Ollama provider
uv add "dynamic-subgraphs[api]"          # + FastAPI HTTP surface
uv add "dynamic-subgraphs[all]"          # everything

Or with pip:

pip install dynamic-subgraphs
pip install "dynamic-subgraphs[openai]"  # same extras: anthropic, ollama, api, all

The core install is intentionally light (langgraph, langchain-core, pydantic, python-dotenv); provider SDKs and the API server are optional extras so you only pull what you use.

Quickstart (development)

# Set up the dev environment (all extras + dev tooling)
uv sync --all-extras

# Run the offline mock demo (free, no tokens)
uv run python -m app.main "compare A and B"

# Run the HTTP API (boots in mock mode by default; needs the `api` extra)
uv run python -m app.api

By default everything runs in mock mode — free and offline. Set DS_PLANNER=llm and DS_PROVIDER=<provider> to use the real planner and grounded tools. The legacy DS_PLANNER=openai value still maps to planner=llm with provider=openai.

Built-in providers (default_model_providers()):

DS_PROVIDER Package Credentials
openai langchain-openai OPENAI_API_KEY
anthropic langchain-anthropic ANTHROPIC_API_KEY
ollama langchain-ollama none (local server; OLLAMA_BASE_URL optional)

Each role (planner, worker, reducer, subagent, judge) can target a different provider/model through RunConfig's role-specific ModelRef fields; unset roles fall back to the worker model, then to the base provider+model.

SDK usage

The dynamic_subgraphs package is the importable facade — build an EngineConfig, hand it to the engine, then call run():

from dynamic_subgraphs import DynamicSubgraphs, EngineConfig, Model

# Cloud (key from env)
engine = DynamicSubgraphs(EngineConfig(model=Model("openai", "gpt-5.4-nano")))

# ...or a local LM Studio / Ollama server (bring your own URL/key/model)
engine = DynamicSubgraphs(EngineConfig(model=Model.lmstudio("google/gemma-3-27b")))
engine = DynamicSubgraphs(EngineConfig(model=Model.ollama("llama3.1")))

result = engine.run("Compare two sources on X and recommend one.")
result.response      # synthesized answer text
result.values        # {output_key: value, ...}
result.plan          # the generated GraphSpec
result.artifacts     # {filename: Path} (populated only when recording is on)
result.usage         # exact TokenUsage: input/output/total + per-model breakdown
result.cost          # USD (None unless a pricing book is configured — see below)

Token usage & cost

result.usage is always populated with the providers' own reported token counts (via LangChain's usage callback — exact, all providers, no estimation). Cost is opt-in: pass a pricing book on EngineConfig (we don't ship a price table since prices drift). Key it by the model alias — it also matches the provider's dated snapshot (gpt-5.4-nano-2026-03-17) by prefix:

engine = DynamicSubgraphs(EngineConfig(
    model=Model("openai", "gpt-5.4-nano"),
    pricing={"gpt-5.4-nano": {"input_per_1m": 0.20, "output_per_1m": 1.25}},
))
r = engine.run("...")
r.usage.total_tokens   # e.g. 3233   (exact, free)
r.cost                 # e.g. 0.0013 (USD; None if no pricing)

If you already use LangSmith, it computes cost server-side too — no price book needed there.

All engine configuration lives on EngineConfig: the per-role models (model, planner_model, worker_model, reducer_model, subagent_model, judge_model), the recording policy, planner mode, runs_dir, providers, and checkpointer.

⚠️ Use a capable model for the planner. The planner must emit a valid GraphSpec; small local models (7B-class, and in practice anything below ~20–30B) frequently produce invalid plans and fail. Run small/local models as the worker_model with a stronger planner_model.

Recording (opt-in)

By default the engine writes no files — embedding it never clutters your working tree. Suggestion: set a recording policy while developing or debugging to capture the trace under runs/<run_id>/, then leave it at the default in production / library use:

from dynamic_subgraphs import Recording, Artifact

engine = DynamicSubgraphs(EngineConfig(
    model=Model("openai", "gpt-5.4-nano"),
    recording=Recording.debug(),      # capture everything
    runs_dir="runs",
))

Recording is granular — choose exactly which artifacts to write with the Artifact enum (its values are the filenames) and the Recording policy:

recording=Recording.visual_only()           # just graph.mmd (the diagram)
recording=Recording.all() - {Artifact.SPEC}  # everything except spec.json
recording={Artifact.MERMAID, Artifact.TRACE}  # a raw set works too
Preset Writes Use for
Recording.none() (default) nothing embedding / production
Recording.all() every artifact full capture
Recording.debug() every artifact debugging a run
Recording.visual_only() graph.mmd a picture of the graph
Recording.replayable() spec.json + output.json enabling resume/replay

Coding agents can enumerate every valid option via DynamicSubgraphs.capabilities(). See docs/recipes.md for copy-pasteable patterns.

Engine model defaults can be overridden per run() call, so each run picks the models for its own node calls (e.g. a cheap cloud planner with local workers):

result = engine.run(
    "Investigate this task.",
    planner_model=Model("openai", "gpt-5.4-nano"),
    worker_model=Model.lmstudio("openai/gpt-oss-20b"),
)

Documentation

  • examples/ — runnable, standalone SDK integration examples (one file per pattern).
  • docs/recipes.md — copy-pasteable SDK patterns (local models, hybrid, recording presets, debugging) + tested-model and latency tables.
  • docs/evals/ — eval reports (e.g. the gpt-5.4-nano vs claude-haiku-4-5 e2e comparison: latency / tokens / cost / quality, traced via LangSmith).
  • docs/api.md — the HTTP surface over the supervisor (endpoints, modes, auth, examples).
  • docs/dynamic-graphs-canonical-design-v1.md — canonical project design and source of truth.
  • docs/index.md — full documentation index.
  • AGENTS.md — agent-facing package map and MVP sequence.

Contributing & support

Contributions are welcome — see CONTRIBUTING.md for the dev setup, test, and formatting workflow, and CODE_OF_CONDUCT.md. Found a bug or have a request? Open an issue. For security reports, see SECURITY.md.

Status

Pre-1.0 (0.x) — the public SDK surface is usable and tested, but the API may change between minor versions until 1.0. See CHANGELOG.md.

License

Licensed under the Apache License 2.0. See NOTICE for attribution.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dynamic_subgraphs-0.1.0.tar.gz (173.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dynamic_subgraphs-0.1.0-py3-none-any.whl (120.9 kB view details)

Uploaded Python 3

File details

Details for the file dynamic_subgraphs-0.1.0.tar.gz.

File metadata

  • Download URL: dynamic_subgraphs-0.1.0.tar.gz
  • Upload date:
  • Size: 173.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dynamic_subgraphs-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c158d664f1a28bf5de824b412caf68efe59d42ec639b14ac76eb00a350ff56fd
MD5 0f28b708c1a0fbd9ecfebf6d20fbbd7d
BLAKE2b-256 2000367972bcb8fd73e528cce3dee24cf8bed5f7664425da12d96bcfcc0ea756

See more details on using hashes here.

Provenance

The following attestation bundles were made for dynamic_subgraphs-0.1.0.tar.gz:

Publisher: publish.yml on Ian-Tharp/Dynamic-Subgraphs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dynamic_subgraphs-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for dynamic_subgraphs-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1b6ef4b8a18b880aa0a599c4a505a75cf9219453595b99fd86eda27852a6feb3
MD5 40de9ff0e5dbe3e0e4e791da6c37150a
BLAKE2b-256 691e75d5e3fbbf99dd1a98b8eec0dc5e9b275a022e32e7c4d79a0c62823a79dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for dynamic_subgraphs-0.1.0-py3-none-any.whl:

Publisher: publish.yml on Ian-Tharp/Dynamic-Subgraphs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page