Orientation for coding agents working in this repo — i.e., agents
contributing to openarmature itself. README.md covers what the project
is and how to use it; this file covers things that aren't obvious from
reading the code.
Two AGENTS.md files in this project. Different audiences.
- This file (
./AGENTS.md, at the repo root) — for agents working on the openarmature codebase. Package layout, test layout, tooling, spec-submodule discipline, commit conventions.src/openarmature/AGENTS.md(shipped in the wheel) — for agents working in user codebases that depend on openarmature. Capability contracts, common patterns, non-obvious shapes, example index. Generated byscripts/build_agents_md.pyfrom canonical sources; committed and CI-drift-checked.
This repo is a Python implementation of
openarmature-spec.
Behavior is defined by the spec; this repo executes it.
- The spec lives at
openarmature-spec/as a git submodule pinned to a released tag. Don't edit files in the submodule. - The pin tracks the latest Accepted spec version, not the tip of
openarmature-spec/main.mainmay contain Draft proposals merged into the spec text; only Accepted releases ship normative behavior, and the implementation conforms to the pinned Accepted release. - Behavior changes that aren't already in the Accepted spec require a proposal in the spec repo first, not a PR here.
- To bump the spec submodule after a new Accepted release:
cd openarmature-spec && git checkout <tag>, then bump the three places that track the version (below).
Proposals travel Draft → Accepted in the spec repo (see
openarmature-spec/GOVERNANCE.md for the format and flow). Proposals
live in openarmature-spec/proposals/; canonical spec text in
openarmature-spec/spec/<capability>/.
When implementing a feature, read the relevant Accepted proposal first — don't infer behavior from existing impl alone. Draft proposals don't ship; their text may change before acceptance.
tool.openarmature.spec_versioninpyproject.toml__spec_version__insrc/openarmature/__init__.py- The submodule commit (must match a released spec tag, e.g.
v0.10.0)
tests/test_smoke.py asserts the first two match. The third is enforced
by convention.
src/openarmature/graph/— graph engine (State, GraphBuilder, CompiledGraph, edges, projections, fan-out)src/openarmature/llm/— LLM Provider Protocol + OpenAIProvider; HTTP error classification + retry helperssrc/openarmature/checkpoint/— checkpointing protocol + in-memory and filesystem backendssrc/openarmature/observability/—[otel]extra; OTel observer + log bridge + correlation primitivessrc/openarmature/middleware/— pipeline-utility middleware
tests/conformance/— runs the spec's YAML fixtures against the engine via an adapter. Drives most of the behavior coverage.tests/unit/— fills coverage gaps the conformance suite doesn't reach:edge_exception,reducer_error,state_validation_error,SubgraphNode.run, projection variants, frozen-state mutation, etc.tests/test_smoke.py— version sync.
A green conformance run is the null result, not evidence. A fixture
wired into a driver that ignores half its expected block passes exactly
like one that is fully asserted, so "it passes" distinguishes nothing.
The acceptance criterion is therefore inverted: a fixture is wired when you have broken the behaviour it covers and watched it go red.
- Mutate the src behaviour, not the test. Confirm the mutation actually landed before trusting the result: if the anchor text occurs twice and you changed one, a partial mutation reads exactly like a surviving one.
- A fixture that passes the moment you add a dispatch entry, with no harness work, is the highest-risk case, not the easiest win.
- Diagnose by running, not by reading. A deferral reason reached by
reasoning about which driver "looks" suitable is routinely wrong; a
ten-second
await _run_x(_load(path))settles it. - Prefer a structural guard over remembering any of this, but check its
reach before trusting it. The
_DRIVER_EXPECTED_KEYScheck intests/conformance/test_observability.pycatches "wired into a driver that drops a directive" — it caught a live error in the commit that added it — but only for the drivers registered in that map, which is a minority of them. It is fail-open: a fixture routed to an unregistered driver returns silently, so a green run there still means nothing. Registering the driver is what turns the guard on.
This is written down because the rule existed as guidance and was still missed repeatedly across one session: four fixtures were reported wired while asserting nothing, and three deferral reasons were wrong.
uvfor everything. Don't usepipdirectly.- Pyright strict mode is enforced (
pyproject.toml). Annotations are not optional. - Ruff for lint + format. Pre-commit hook runs
ruff formatautomatically — the file you committed may not be the file in the next diff. pytest-asynciowithasyncio_mode = "auto"—async def test_...works with no decorator.
uv run pytest -q # all tests
uv run pytest tests/conformance/ -v # spec conformance only
uv run ruff check . && uv run ruff format # lint + format
uv run pyright src/ tests/ # type check
uv run mkdocs serve # preview the docs site locallyBranch names use <type>/<kebab-case-description> (3–5 words). Allowed
types:
feature/— new functionalityfix/— bug fixesrefactor/— restructuring without behavior changechore/— tooling, deps, config, housekeepingschema/— data model changes
For ticketed work, embed the ID:
feature/PROJ-123-short-description.
Commit subjects follow the 50/72 rule — subject ≤ 50 chars (hard cap 72), imperative mood, capitalized, no trailing period. Body wrapped at 72 columns. Body explains what and why, not how.
User-facing docs live in docs/ and build via MkDocs Material; the
deployed site is at openarmature.ai. CI build + deploy is in
.github/workflows/docs.yml. Local preview: uv run mkdocs serve.
They have different audiences, and the split is not a style preference.
Docstrings are published. mkdocstrings renders them into
docs/reference/*.md and they surface through help(), so a docstring is
shipped end-user documentation. Write for someone calling the API who
cannot see the implementation: what it does, what the arguments and return
mean, what it raises, and any constraint the caller has to honour. If a
sentence only makes sense to someone editing the body, it is not a
docstring.
# comments are for maintainers, and carry everything a caller does
not need: spec section references, MUST / SHOULD / MAY rules, rationale,
rejected alternatives, and why a line is the way it is. Keep them short
per the comment rules in the global CLAUDE.md.
So these move out of a docstring and into a # comment:
- Spec citations (
§8.4.2,proposal 0119,spec v0.116.0) and bare prose like "the spec defines" or "the spec requires". - Normative language about what an implementation MUST or MAY do. A caller does not implement the spec; we do.
- Rationale for the implementation, and comparisons to how another module or observer handles the same thing.
Two things stay in docstrings even though they look like spec references:
a spec/ path (it is a location, not a normative claim) and a
parameter genuinely named spec.
Applies to tests/ too. A test docstring is read by whoever is deciding
whether the test still earns its place.
Health check: the median docstring here is 6 lines. Past about 20, ask whether the extra is caller-facing documentation or maintainer notes that drifted in.
Stateisfrozen=TrueANDextra="forbid". Nodes that return an undeclared field surface as astate_validation_error, not a silent drop.- Conditional edges over-approximate at compile time (a conditional from node X is treated as reaching every node), so the unreachable-node check is sound but not tight.
- Each node has exactly one outgoing edge. Branching is via conditional edges, not multiple statics.
ENDis a distinct sentinel object, not a reserved string. Use the exportedENDconstant.
In scope:
- Graph engine + the spec's runtime contract.
- Pipeline utilities (rate limiting, structured-output retry helpers).
- Observability via OTel observer (under
[otel]extra). - Checkpointing (in-memory + filesystem backends).
- LLM Provider Protocol + the canonical OpenAI implementation.
Out of scope, deferred to sibling packages at v1.0:
openarmature-otel— eventual extraction of the OTel observer for projects that don't want it in core.openarmature-eval— evaluation framework.
Behavior changes outside the Accepted spec require a spec proposal first.