Skip to content

Commit 554437d

Browse files
Refresh README and docs homepage pitch (#114)
* Refresh README and docs homepage pitch Replace the README's 10-bullet "Why OpenArmature" feature inventory with 5 differentiating reasons-to-choose, ordered for an engineer or architect evaluating OA against alternatives: 1. Workflows to agents, one engine (LLM-infused pipelines first; agents and deterministic ETL as the spectrum endpoints). 2. Crash-safe resume by spec contract (built for preemptible compute and queue workers, not just human-in-the-loop interrupts). 3. Destination-pluggable observability (native OTel for vendor neutrality, separate native LangfuseObserver for teams on Langfuse, no SaaS lock-in to a framework-vendor-owned product). 4. Bad graphs don't compile (.compile rejects six categories of structural error before invoke is reachable). 5. Spec, not just code (public language-agnostic spec with conformance fixtures bounding the future surprise space). Match the same positioning on docs/index.md's homepage card grid. Swap the six feature-inventory cards (typed-state, compile-time, observable, checkpointable, fan-out, async/LLM-agnostic) for six reason-to-choose cards mirroring the README order, with a sixth card capturing parallelism (fan-out + parallel-branches + nested attribution correctness, formalized) that's genuinely differentiated against LangGraph's Send-based primitive. Existing card icons retained. "Open specification" section stays at the bottom of the homepage. * Tighten pitch wording per PR review Three CoPilot findings on PR #114: 1. docs/index.md card 5 used "sub-graphs"; the canonical spelling across both code and docs is "subgraphs" (no hyphen). Normalized. 2. docs/index.md card 4 and README.md card 4 both claimed .compile() rejects "undeclared subgraph fields". The actual error (MappingReferencesUndeclaredField) fires for both side='parent' AND side='subgraph' across the fan_out.{items_field, target_field, collect_field, inputs, extra_outputs} checks; the previous wording was too narrow. Reworded to "mappings to undeclared state fields" so both surfaces match each other and the actual error shape. 3. CHANGELOG bullet framed the change as "around 5 reasons-to-choose" then enumerated 6 homepage cards. Reworded the bullet to acknowledge both counts (5 in README; 5 + retained async-first card on the homepage = 6).
1 parent 22fc2fd commit 554437d

3 files changed

Lines changed: 36 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
88

99
### Changed
1010

11+
- **README and docs homepage refreshed around reasons-to-choose.** Replaced the 10-bullet "Why OpenArmature" feature inventory in `README.md` with 5 differentiating reasons (LLM-infused workflows to agents on one engine; crash-safe resume by contract; destination-pluggable observability with OTel + Langfuse, no SaaS lock-in; compile-time topology checks; spec + conformance). The docs homepage (`docs/index.md`) card grid carries the same five plus a sixth card retained from the previous grid for async-first / LLM-agnostic: workflows-to-agents, crash-safe, pluggable observability, bad-graphs-don't-compile, parallelism (fan-out + parallel-branches + nested correctness), async-first.
1112
- **Docs sweep: stale references and em-dash normalization.** Fixed three definite stale references (`spec_version='0.26.0'` in the Langfuse example output now reads `'0.38.0'`; the dangling `v0.16.1` qualifier dropped from the parallel-branches concept page; `compiled.attach_observer` corrected to `graph.attach_observer` in `non-obvious-shapes.md` for variable-name consistency with the rest of the docs). Swept em dashes out of the user-facing docs (130 instances across 17 files) per the convention set during the patterns expansion. mkdocs strict build clean; no broken intra-docs links.
1213

1314
### Added

README.md

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,38 +26,22 @@ pip install 'openarmature[otel]'
2626

2727
## Why OpenArmature
2828

29-
**State you can't accidentally mutate.**<br>
30-
State schemas are frozen Pydantic models. Nodes return partial updates; the engine merges. The snapshot a node holds can't change mid-execution, and assignment into state raises rather than silently writing.
29+
**One framework, from LLM-infused workflows to tool-calling agents.**<br>
30+
OpenArmature is built for LLM pipelines first: extract, classify, route, render, validate, persist, with the LLM dropped in wherever probability beats hand-written rules. Tool-calling agents (graphs that cycle back to an LLM node) and pure deterministic ETL (no LLM at all) sit at the two ends of the same spectrum. The graph engine has zero concept of LLMs, tools, or messages; those live at the node boundary behind a `Provider` Protocol. One platform for the whole gradient, instead of bending agent-shaped frameworks to fit workflow-shaped work.
3131

32-
**Schema validation at every merge.**<br>
33-
Fields outside the declared schema fail at the merge boundary instead of silently dropping. A node returning `{"plann": "..."}` (typo) raises `StateValidationError` immediately, not three nodes downstream when the field is read and doesn't exist.
32+
**Crash-safe resume is first-class, by spec contract.**<br>
33+
Every completed node is followed by a synchronous checkpoint save before the engine advances. Any node fails, the process dies, OOM kill, preemption: the next `invoke(resume_invocation=...)` picks up from the last saved state with a fresh `invocation_id` (audit trail) and the original `correlation_id` preserved (cross-system join). Explicit state-schema migration registration handles old in-flight checkpoints when the schema evolves. Built for preemptible compute, queue workers, and any environment where the process can die mid-step, not just for human-in-the-loop interrupt resume.
3434

35-
**Merge policy on the schema, not the call site.**<br>
36-
Each state field declares its reducer (`last_write_wins`, `append`, `merge`, or a user-defined callable) as part of the schema. Two nodes writing the same field compose via the field's policy: once, declaratively, instead of duplicated across call sites.
35+
**Destination-pluggable observability, not anchored to a paid SaaS.**<br>
36+
`OTelObserver` (in `openarmature[otel]`) emits the OpenTelemetry GenAI semantic conventions (`gen_ai.system`, `gen_ai.request.*`, `gen_ai.response.*`, `gen_ai.usage.*`) that Honeycomb, HyperDX, Phoenix, Datadog APM, Tempo, an open-source Jaeger, or your own OTLP collector all render natively without per-service shims. On top of that, a separate `LangfuseObserver` (in `openarmature[langfuse]`) provides a native mapping for teams who've chosen Langfuse: MIT-licensed, self-hostable, decoupled through a `LangfuseClient` Protocol so swapping it out is a single-file change. No coupling to a closed-source product owned by the framework vendor.
3737

38-
**Subgraphs compose with explicit data seams.**<br>
39-
Subgraphs run against their own state schema with `inputs` (additive, opt in to share parent fields) and `outputs` (replacement, name exactly what comes back) mappings. Parent fields don't leak in by accident; subgraph fields don't slip out unless declared.
38+
**The graph either compiles or it never runs.**<br>
39+
`.compile()` rejects six categories of structural error before `invoke()` is reachable: unreachable nodes, dangling edges, conflicting reducers, no declared entry, mappings to undeclared state fields, multiple outgoing edges from one node. State schemas are frozen Pydantic models validated at every merge boundary. For a 30-node pipeline with conditional routing, the difference between "tests pass" and "tests pass on today's code path" is structural.
4040

41-
**Bad graphs don't compile.**<br>
42-
Dangling edges, unreachable nodes, conflicting reducers, no declared entry, mappings to undeclared fields, multiple outgoing edges from one node. Six categories of structural error all fail at `.compile()`, not at runtime mid-execution. The graph either constructs cleanly or it doesn't reach `invoke()`.
41+
**There's a spec, not just code.**<br>
42+
OpenArmature is defined by a public, language-agnostic [specification](https://github.com/LunarCommand/openarmature-spec) with conformance fixtures every reference implementation must pass. Behavior is bounded by the spec; implementations conform to it. Minor-version surprises around state merge, fan-out collection, or resume semantics live in proposals tracked openly, not in silent code changes between releases.
4343

44-
**The graph engine has no concept of LLMs or tools.**<br>
45-
Validation, retry, recovery, structured output: those are node-internal or middleware concerns. The same engine runs deterministic ETL pipelines and tool-calling agents; the topology layer doesn't pick a side.
46-
47-
**Determinism is a contract.**<br>
48-
Same input, same node implementations, same edge functions, same final state, and same observed node-execution order. The spec mandates it; conformance fixtures verify it across every implementation. Replay an audit run and get byte-identical state.
49-
50-
**Checkpoint saves are synchronous-by-contract.**<br>
51-
The engine awaits each save before advancing. A crash immediately after a `completed` event cannot have lost the corresponding write. Resume mints a fresh `invocation_id` (audit trail) while preserving `correlation_id` (cross-system join key), so a recovered run is traceable as a new attempt without losing the thread to the original request.
52-
53-
**Observability that doesn't double-export.**<br>
54-
The OpenTelemetry mapping mandates a private `TracerProvider`. That prevents the trap where global-provider auto-instrumentation libraries (OpenInference, Langfuse v3, etc.) emit duplicate spans alongside the framework's. Your spans flow exactly where you point them; no surprise fan-out to vendor backends you didn't configure.
55-
56-
**LLM spans LLM-aware backends can actually read.**<br>
57-
Each `provider.complete()` call emits a dedicated `openarmature.llm.complete` span carrying both the framework's `openarmature.llm.*` attributes and the cross-vendor OpenTelemetry GenAI semantic conventions (`gen_ai.system`, `gen_ai.request.*`, `gen_ai.response.*`, `gen_ai.usage.*`). Langfuse, Phoenix, Honeycomb's LLM lens — they render generations correctly out of the box, no per-service attribute-mapping shim required. Input/output payload emission is opt-in (`disable_llm_payload=False`), default-off because the payload may contain PII; image bytes are unconditionally redacted at the provider so they never enter the observability stream.
58-
59-
**Native Langfuse mapping, not just OTLP.**<br>
60-
Alongside the OpenTelemetry mapping, `LangfuseObserver` (in `openarmature[langfuse]`) maps invocations to Langfuse Traces and Observations directly — subgraph hierarchy, per-instance fan-out, and detached-trace mode included. Both observers can run on one graph. Caller-supplied invocation metadata (`invoke(metadata={"tenantId": ...})`) propagates to every backend at once: `openarmature.user.*` span attributes on the OTel side, top-level `trace.metadata` / `observation.metadata` keys on the Langfuse side.
44+
For the full feature catalog see [openarmature.ai/concepts](https://openarmature.ai/concepts/).
6145

6246
## Hello World
6347

docs/index.md

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,49 +16,54 @@ buy-in from every node.
1616

1717
<div class="grid cards" markdown>
1818

19-
- :material-shield-check:{ .lg .middle } &nbsp; __Typed, frozen state__
19+
- :material-shield-check:{ .lg .middle } &nbsp; __Workflows to agents, one engine__
2020

2121
---
2222

23-
State schemas are Pydantic models with `frozen=True` and
24-
`extra="forbid"`. Nodes can't mutate state; they return partial
25-
updates and the engine merges via per-field reducers.
23+
Built for LLM-infused pipelines first. Tool-calling agents (cycle
24+
back to an LLM node) and pure deterministic ETL sit at the two ends
25+
of the same spectrum.
2626

27-
- :material-graph:{ .lg .middle } &nbsp; __Compile-time checks__
27+
- :material-content-save:{ .lg .middle } &nbsp; __Crash-safe by contract__
2828

2929
---
3030

31-
Bad graph shapes (dangling edges, unreachable nodes, conflicting
32-
reducers, missing entry) fail at `.compile()`, not at run time.
31+
Synchronous checkpoint save after every node. Process dies
32+
mid-step, next `invoke(resume_invocation=...)` picks up from the
33+
last save with `correlation_id` preserved.
3334

34-
- :material-eye:{ .lg .middle } &nbsp; __Observable, opt-in__
35+
- :material-eye:{ .lg .middle } &nbsp; __Pluggable observability__
3536

3637
---
3738

38-
Attach an `Observer` to see every node boundary. Drop in the
39-
optional OTel mapping for spans + log correlation; logs carry
40-
`trace_id` / `span_id` / `correlation_id` automatically.
39+
Native `OTelObserver` emits GenAI semantic conventions any OTLP
40+
backend renders. Separate `LangfuseObserver` for the Langfuse
41+
destination. No vendor lock-in to a paid SaaS.
4142

42-
- :material-content-save:{ .lg .middle } &nbsp; __Checkpointable__
43+
- :material-graph:{ .lg .middle } &nbsp; __Bad graphs don't compile__
4344

4445
---
4546

46-
In-memory and SQLite `Checkpointer` backends ship in core. Crash at
47-
node N+1, resume from node N's saved state on the next invocation.
47+
`.compile()` rejects six categories of structural error before
48+
`invoke()` is reachable: dangling edges, unreachable nodes,
49+
conflicting reducers, no entry, mappings to undeclared state
50+
fields, multiple outgoing edges.
4851

49-
- :material-arrow-split-vertical:{ .lg .middle } &nbsp; __First-class fan-out__
52+
- :material-arrow-split-vertical:{ .lg .middle } &nbsp; __Parallelism, formalized__
5053

5154
---
5255

53-
Per-instance fan-out with bounded concurrency, error-policy choice,
54-
and observability events that attribute correctly per instance.
56+
Fan-out with bounded concurrency and per-instance error policy.
57+
Parallel-branches runs N named subgraphs. Both nest with
58+
attribution-correct observability.
5559

5660
- :material-language-python:{ .lg .middle } &nbsp; __Async-first, LLM-agnostic__
5761

5862
---
5963

60-
The engine has no concept of LLMs or tools; those live at the node
61-
boundary. Use any provider, any model, any external system.
64+
asyncio-native throughout: every node, observer, and checkpointer
65+
is `async`. Use any LLM provider, any model, any external system.
66+
Drops directly into FastAPI lifespan hooks.
6267

6368
</div>
6469

0 commit comments

Comments
 (0)