This note sketches a concrete first layout for a Python-facing agent stack on top of weaveback.
The design goal is narrow on purpose:
-
Rust remains the source of truth for project state, tracing, and mutation.
-
Python becomes the typed orchestration layer for agent loops.
-
Mutations go through a typed
ChangePlan, not arbitrary file writes. -
The core system stays usable without shell-first workflows or editor-specific glue.
The result is a three-layer stack:
-
weaveback-agent-core— pure Rust application API over existing weaveback primitives -
weaveback-py— a thin PyO3 extension module -
python/weaveback-agent— a modern Python package with Pydantic models and agent loop
Why This Split
The main risk in adding Python is accidentally creating a second, weaker source of truth. If Python can patch files directly, it will bypass the very thing that makes weaveback interesting: edits are traceable back to literate sources and can be oracle-verified against generated output.
That is why the split is asymmetric:
-
Rust owns reads from the database, source maps, chunk context, and edit application.
-
Python owns typed planning, orchestration, and model integration.
-
The PyO3 boundary is intentionally thin so it is easy to audit.
This also keeps the future Helix side clean. Helix can render plans, previews, and diagnostics, but it does not need to become the execution substrate.
Why maturin + uv
For a new Python extension in 2026, the practical default is:
-
maturinfor building and publishing the PyO3 extension -
uvfor environment management, dependency locking, and task execution -
src/layout for the Python package
This avoids the older split where Rust packaging and Python packaging drift
apart. maturin keeps the extension story standard, and uv keeps Python
tooling reproducible without turning the system into a shell script nest.
System Diagram
The architecture is easiest to reason about if reads and writes are shown as separate channels.
Mutation Sequence
The write path should stay boring and explicit.
Workspace Shape
The first pass should add two Rust crates and one Python project:
crates/
weaveback-agent-core/
Cargo.toml
src/
lib.rs
workspace.rs
read_api.rs
change_plan.rs
apply.rs
weaveback-py/
Cargo.toml
src/
lib.rs
python/
weaveback-agent/
pyproject.toml
README.md
src/
weaveback_agent/
__init__.py
models.py
agent.py
The separation between weaveback-agent-core and weaveback-py matters for
one reason: the Rust API should still be callable from Rust tests, from a
future local HTTP service, or from the existing MCP server without Python
being on the critical path.
Canonical Sources
This page is an architecture note. It explains the design and shows representative code excerpts, but it does not assemble files.
The canonical literate sources live with the crates they explain:
-
weaveback-agent-core -
weaveback-py
The Python package under python/weaveback-agent/ is ordinary source rather
than a literate subtree. The snippets below describe that public surface; they
are not another source of truth.
Rust Core Crate
The core crate is where the agent-facing API becomes explicit. It should not contain PyO3 types, and it should not know anything about Pydantic.
Cargo.toml
[package]
name = "weaveback-agent-core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
ureq.workspace = true
weaveback-core.workspace = true
weaveback-lsp.workspace = true
weaveback-macro.workspace = true
weaveback-tangle.workspace = true
Public Surface
The public surface should look like an application service, not like a bag of
free functions. A Workspace can cache configuration and reusable handles.
pub use ;
pub use ;
pub use ;
pub use ;
Workspace API
The Workspace type gives Python one stable object to hold. It prevents the
extension layer from growing a sprawling function namespace, and it gives Rust
a place to manage db paths, gen_dir, project root, and future caches.
use crate;
use crateChangePlan;
use crate;
use ;
use PathBuf;
Change Plan Types
The edit model has to be strict enough that Rust can reject unsafe plans before the system mutates anything. The main design choice is that the plan targets source ranges but also carries an output anchor for oracle verification.
use ;
Read Models
The read API should return plain serde models. That keeps the PyO3 layer thin and lets the same types flow through tests and future transports.
use crateWorkspaceConfig;
use ;
use PathResolver;
use ;
use ;
use process_string_precise;
use WeavebackDb;
use ;
Validation and Apply Path
The validation step is where the system enforces policy. The write path should be impossible to reach without a valid plan.
use crateChangePlan;
use crateWorkspaceConfig;
use ;
use ;
use PathResolver;
use ;
use process_string;
use WeavebackDb;
PyO3 Crate
The PyO3 layer should be as dumb as possible. It should convert Python inputs
into Rust structs, call weaveback-agent-core, and convert serde outputs back
into Python values.
This is the wrong place for planning logic, prompt policy, or change-plan semantics.
Because the Python floor here is 3.14, the extension crate should track the
current PyO3 line rather than pinning an older compatibility window.
Cargo.toml
[package]
name = "weaveback-py"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[lib]
name = "_weaveback"
crate-type = ["cdylib"]
[dependencies]
pyo3.workspace = true
pythonize.workspace = true
serde.workspace = true
serde_json.workspace = true
weaveback-agent-core.workspace = true
Extension Module
The methods return Python-native structures, but the real schemas live in the Python package. That gives Python room to evolve the public Pydantic models without forcing Rust-level ABI churn for every small wrapper change.
use PyRuntimeError;
use *;
use ;
use ;
Python Package
The Python package is where agent code should actually live: model wrappers, Pydantic validation, prompt assembly, and the high-level loop that returns a structured plan.
pyproject.toml
[build-system]
requires = ["maturin>=1.8,<2"]
build-backend = "maturin"
[project]
name = "weaveback-agent"
version = "0.1.0"
description = "Typed Python agent APIs for weaveback"
readme = "python/weaveback-agent/README.md"
requires-python = ">=3.14"
dependencies = [
"pydantic>=2.11",
]
[dependency-groups]
dev = [
"maturin>=1.13.1",
"pyright>=1.1.400",
"pytest>=8.3",
"ruff>=0.11",
]
[tool.maturin]
module-name = "weaveback_agent._weaveback"
python-source = "python/weaveback-agent/src"
manifest-path = "crates/weaveback-py/Cargo.toml"
[tool.ruff]
line-length = 100
[tool.pyright]
pythonVersion = "3.14"
typeCheckingMode = "strict"
README.md
The Python project should explain that Rust owns project mutation and that the package is an orchestration layer over weaveback rather than a separate editor backend.
- --
Package Entry Point
The package root should expose the typed surface, not the raw extension.
=
Pydantic Models
These models are the public contract for Python callers and for any PydanticAI-style loop that asks the LLM to produce typed data.
=
=
=
=
=
=
=
:
: =
: =
:
:
:
=
return
:
:
:
:
:
:
:
:
:
:
: =
:
=
=
return
:
:
:
:
:
:
:
:
:
:
:
:
:
:
: | None = None
: | None = None
: | None = None
: | None = None
: | None = None
: | None = None
Agent Loop
The loop below is intentionally small. It reads with weaveback primitives,
asks a planner for a typed ChangePlan, validates the plan in Rust, previews
the plan in Rust, and returns structure instead of applying by default.
:
: | None = None
: | None = None
: | None = None
: =
=
=
return None
return
=
return
=
return
del
del
return
Python Integration Tests
These tests validate the supported path for the PyO3 extension: install it
with maturin develop, then import it from Python and exercise the thin agent
wrapper over a real temporary workspace.
= /
= /
=
= /
= /
=
=
assert in
assert is None
assert is None
Next Step
The next implementation step should not be "add more agent features". It should be:
-
extract reusable read/apply logic out of
crates/weaveback/src/mcp.rsinto a Rust library surface -
make
weaveback-agent-corecall that surface -
make
weaveback-pycallweaveback-agent-core
That keeps one mutation engine and one tracing engine, which is the main thing worth preserving.