Skip to content

Commit 472126d

Browse files
docs: re-read README, Quickstart, Concepts (#55)
Editorial pass after the 0.6.x feature wave. - README: grammar fix on the engine-features sentence; Next-steps Concepts/Examples bullets updated to include the 0.6.x topics (parallel branches, LLMs, prompts) and link to docs/examples rather than the GitHub directory. - Quickstart: Concepts and Examples link lists refreshed (was missing parallel-branches/llms/prompts/checkpointing; said "five demos" with ten now shipped). - concepts/index: added Prompts; expanded llms and checkpointing blurbs. - concepts/graphs: added the four 0.6.x builder methods (add_fan_out_node, add_parallel_branches_node, with_checkpointer, with_state_migration) to the methods list. - concepts/composition: appended a "Related composition primitives" section pointing at fan-out and parallel-branches. - concepts/fan-out: switched code samples to the canonical builder.add_fan_out_node(...) API. - concepts/parallel-branches, observability: em-dash sweep. - concepts/llms: new Tool calling section between Structured output and Content blocks; refreshed example links to point at docs/examples instead of the GitHub repo. state-and-reducers, checkpointing, and prompts pages audited and left untouched (already accurate against v0.6.x).
1 parent e125494 commit 472126d

9 files changed

Lines changed: 130 additions & 52 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
### OpenArmature is a workflow framework for LLM pipelines and tool-calling agents.
1212

13-
Typed state, compile-time topology checks, and observability and crash-safe checkpoints are baked into the engine. The graph layer itself has no concept of LLMs or tools, so the same primitives drive deterministic ETL pipelines and tool-calling agents alike.
13+
Typed state, compile-time topology checks, observability, and crash-safe checkpoints are baked into the engine. The graph layer itself has no concept of LLMs or tools, so the same primitives drive deterministic ETL pipelines and tool-calling agents alike.
1414

1515
This Python package is the reference implementation. The behavioral contract is specified in [openarmature-spec](https://github.com/LunarCommand/openarmature-spec) and verified by conformance fixtures.
1616

@@ -187,8 +187,8 @@ A few things to notice:
187187
## Next steps
188188

189189
- **Quickstart**: build your first graph end-to-end. [openarmature.ai/getting-started](https://openarmature.ai/getting-started/)
190-
- **Concepts**: typed state, reducers, composition, fan-out, checkpointing, observability. [openarmature.ai/concepts](https://openarmature.ai/concepts/)
190+
- **Concepts**: typed state, reducers, graphs, composition, fan-out, parallel branches, LLMs, prompts, observability, checkpointing. [openarmature.ai/concepts](https://openarmature.ai/concepts/)
191191
- **Model Providers**: implement the Provider Protocol for a custom LLM backend. [openarmature.ai/model-providers/authoring](https://openarmature.ai/model-providers/authoring/)
192192
- **API reference**: auto-generated from docstrings. [openarmature.ai/reference](https://openarmature.ai/reference/)
193-
- **Examples**: runnable demos. [openarmature-python/examples/](https://github.com/LunarCommand/openarmature-python/tree/main/examples)
193+
- **Examples**: ten runnable demos with walk-throughs. [openarmature.ai/examples](https://openarmature.ai/examples/) (source at [./examples/](./examples/))
194194
- **Spec**: behavioral contract this implementation conforms to. [LunarCommand/openarmature-spec](https://github.com/LunarCommand/openarmature-spec)

docs/concepts/composition.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,17 @@ the shape or it doesn't; the type checker verifies at use sites. If
277277
you have Java instincts ("where's the `implements` keyword?"), reach
278278
for TypeScript or Go interface instincts instead; that's the same
279279
family.
280+
281+
## Related composition primitives
282+
283+
Subgraphs run once per outer-graph entry into them. Two related
284+
primitives run subgraphs multiple times or in parallel; both use
285+
the same projection machinery at their boundaries.
286+
287+
- [Fan-out](fan-out.md): dispatch N copies of *one* compiled subgraph
288+
against an input collection. Use when you have a list of similar
289+
items to process independently.
290+
- [Parallel branches](parallel-branches.md): dispatch M *heterogeneous*
291+
subgraphs concurrently against the same parent state, each with its
292+
own state schema and (optional) middleware. Use when several
293+
independent analyses share a single input.

docs/concepts/fan-out.md

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,29 @@ A fan-out can dispatch instances driven by a list in state
1818
**`items_field` mode**: one instance per item in a parent list field:
1919

2020
```python
21-
from openarmature.graph import FanOutConfig, FanOutNode
22-
23-
scrape_all = FanOutNode(
24-
name="scrape_all",
25-
config=FanOutConfig(
26-
subgraph=scrape_subgraph, # CompiledGraph[ScrapeState]
27-
items_field="urls", # parent list field, one instance per item
28-
item_field="url", # subgraph field that receives each item
29-
collect_field="content", # subgraph field whose value is collected
30-
target_field="contents", # parent list field that receives the collection
31-
concurrency=4,
32-
error_policy="fail_fast", # or "collect"
33-
on_empty="raise", # or "noop"
34-
),
21+
builder.add_fan_out_node(
22+
"scrape_all",
23+
subgraph=scrape_subgraph, # CompiledGraph[ScrapeState]
24+
items_field="urls", # parent list field, one instance per item
25+
item_field="url", # subgraph field that receives each item
26+
collect_field="content", # subgraph field whose value is collected
27+
target_field="contents", # parent list field that receives the collection
28+
concurrency=4,
29+
error_policy="fail_fast", # or "collect"
30+
on_empty="raise", # or "noop"
3531
)
36-
builder.add_node("scrape_all", scrape_all)
3732
```
3833

3934
**`count` mode**: fixed-or-dynamic instance count, no list field:
4035

4136
```python
42-
fan_out = FanOutNode(
43-
name="sample",
44-
config=FanOutConfig(
45-
subgraph=sample_subgraph,
46-
count=8, # int or callable: state -> int
47-
collect_field="reading",
48-
target_field="readings",
49-
concurrency=4,
50-
),
37+
builder.add_fan_out_node(
38+
"sample",
39+
subgraph=sample_subgraph,
40+
count=8, # int or callable: state -> int
41+
collect_field="reading",
42+
target_field="readings",
43+
concurrency=4,
5144
)
5245
```
5346

docs/concepts/graphs.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ The methods you'll use:
117117
- **`.add_subgraph_node(name, compiled, projection=None)`**: register
118118
a compiled graph as a node inside this graph (see
119119
[Composition](composition.md)).
120+
- **`.add_fan_out_node(name, subgraph=..., ...)`**: dispatch N copies
121+
of one subgraph in parallel (see [Fan-out](fan-out.md)).
122+
- **`.add_parallel_branches_node(name, branches=...)`**: dispatch M
123+
heterogeneous subgraphs concurrently (see
124+
[Parallel branches](parallel-branches.md)).
125+
- **`.with_checkpointer(checkpointer)`**: wire a `Checkpointer`; the
126+
engine saves a record after every `completed` event (see
127+
[Checkpointing](checkpointing.md)).
128+
- **`.with_state_migration(from_version, to_version, migrate)`**:
129+
register one edge of the state-migration chain used when resuming
130+
an older saved invocation (see [Checkpointing](checkpointing.md)).
120131
- **`.set_entry(name)`**: declare where execution begins.
121132
- **`.compile()`**: validate and return `CompiledGraph`.
122133

docs/concepts/index.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ the framework, or jump to whichever concept you need.
1616
heterogeneous subgraphs concurrently with per-branch state schemas
1717
and middleware.
1818
- [LLMs](llms.md): how LLM calls fit into nodes, structured output,
19-
routing on parsed fields, errors at the LLM boundary.
19+
multimodal content blocks, tool definitions, routing on parsed
20+
fields, errors at the LLM boundary.
21+
- [Prompts](prompts.md): versioned templates, composite backends,
22+
prompt-group observability propagation.
2023
- [Observability](observability.md): node-boundary hooks, OTel mapping,
2124
log correlation.
2225
- [Checkpointing](checkpointing.md): save state at each node boundary,
23-
resume from a prior point.
26+
resume from a prior point, schema migration across versions.
2427

2528
If you're brand-new, [Quickstart](../getting-started/index.md) is the
2629
faster entry; under a minute to a running graph. Come back here when

docs/concepts/llms.md

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,58 @@ on every object. Pydantic-derived schemas may need `model_config =
221221
ConfigDict(extra="forbid")` on the class to get the
222222
`additionalProperties: false` in the generated JSON Schema.
223223

224+
## Tool calling
225+
226+
Beyond producing typed text, an LLM call can request work from local
227+
Python functions and resume with their results. The wire shape is a
228+
turn-based loop driven entirely from the same `complete()` call: the
229+
model emits `tool_calls`, the caller dispatches them to local
230+
functions, appends `ToolMessage` responses, and re-calls. The graph
231+
engine has no special concept of tools; the loop fits as a
232+
conditional-edge cycle.
233+
234+
```python
235+
from openarmature.llm import Tool
236+
237+
lookup_mission = Tool(
238+
name="lookup_mission",
239+
description="Look up factual records for a named lunar mission.",
240+
parameters={
241+
"type": "object",
242+
"properties": {
243+
"name": {"type": "string"},
244+
},
245+
"required": ["name"],
246+
"additionalProperties": False,
247+
},
248+
)
249+
250+
response = await provider.complete(messages, tools=[lookup_mission, ...])
251+
```
252+
253+
When the model decides to use one or more tools, the response carries
254+
`finish_reason="tool_calls"` and `response.message.tool_calls` is a
255+
list of `ToolCall(id, name, arguments)` records. `arguments` is a
256+
parsed dict whose shape matches the corresponding tool's `parameters`
257+
schema. The single edge case where `arguments` is `None` is
258+
`finish_reason="error"` for unparseable model output.
259+
260+
The caller dispatches each call to its local function, appends one
261+
`ToolMessage(content=..., tool_call_id=...)` per call to the message
262+
list, and re-calls. The `tool_call_id` field MUST match the
263+
`ToolCall.id` the model emitted so the model can pair its requests
264+
with the responses. The next turn either emits more `tool_calls` or
265+
returns a normal assistant content message signaling completion.
266+
267+
Wiring the loop as a graph cycle: a `call_llm` node, a
268+
`dispatch_tools` node that resolves calls and appends
269+
`ToolMessage`s, a conditional edge from `call_llm` that routes back
270+
to `call_llm` when `tool_calls` are present and forward to a
271+
termination node when they aren't. A turn cap on the routing function
272+
prevents runaway loops on a model that stays in tool-calling forever.
273+
See [`09 - Tool use`](../examples/09-tool-use.md) for the runnable
274+
shape.
275+
224276
## Content blocks (multimodal user messages)
225277

226278
User messages carry content in one of two shapes: a plain text string,
@@ -434,6 +486,10 @@ classifier won't do this for them.
434486
- [API reference: `openarmature.llm`](../reference/llm.md) for the
435487
full surface: message types, `Response`, `RuntimeConfig`, every
436488
error class, validation helpers.
437-
- [Examples: `00-hello-world`](https://github.com/LunarCommand/openarmature-python/tree/main/examples/00-hello-world)
438-
for a runnable graph exercising both `response_schema` forms in one
489+
- [Examples: 00 - Hello, world](../examples/00-hello-world.md) for a
490+
runnable graph exercising both `response_schema` forms in one
439491
pipeline.
492+
- [Examples: 09 - Tool use](../examples/09-tool-use.md) for the
493+
agent-loop pattern with two local tools.
494+
- [Examples: 07 - Multimodal prompt](../examples/07-multimodal-prompt.md)
495+
for content blocks alongside versioned prompts.

docs/concepts/observability.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ A walk-through:
132132

133133
- **`attempt_index`**: 0-based retry attempt counter. `0` for nodes
134134
not wrapped by retry middleware; `1+` for retries. Retry middleware
135-
may wrap transitively — a retry on a [parallel-branches
135+
may wrap transitively. A retry on a [parallel-branches
136136
branch](parallel-branches.md) or fan-out `instance_middleware`
137137
re-runs the whole subgraph; events from inner nodes carry the
138138
wrapping retry's attempt counter.
@@ -148,7 +148,7 @@ A walk-through:
148148
- **`branch_name`**: populated on events from nodes inside a
149149
[parallel-branches branch](parallel-branches.md), carrying the
150150
branch's name as declared on the dispatcher. `None` outside.
151-
Independent of `fan_out_index` both may be present simultaneously
151+
Independent of `fan_out_index`; both may be present simultaneously
152152
when a parallel-branches branch contains a fan-out (or a fan-out
153153
instance contains a parallel-branches node). The combination
154154
`(namespace, branch_name, fan_out_index, attempt_index, phase)`

docs/concepts/parallel-branches.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ insertion order.
66

77
Sibling to [fan-out](fan-out.md) (same `for each thing, do work in
88
parallel` shape), but the *thing* is different per branch: a research
9-
subgraph, a categorize subgraph, a sentiment subgraph each with its
10-
own state schema, its own middleware, its own observer events
9+
subgraph, a categorize subgraph, a sentiment subgraph (each with its
10+
own state schema, its own middleware, its own observer events),
1111
running in parallel and joining their results into one parent state.
1212

1313
## When to reach for parallel branches
@@ -56,14 +56,14 @@ builder.add_parallel_branches_node(
5656
Each branch's `subgraph` is a compiled graph; `inputs` and `outputs`
5757
mirror the explicit projection shape from
5858
[composition](composition.md#explicitmapping-declarative). The
59-
branches dict's key is the branch name used as the branch identity
59+
branches dict's key is the branch name, used as the branch identity
6060
on observer events (see [observability](observability.md)) and in
6161
the per-branch error records that `error_policy: "collect"`
6262
produces.
6363

6464
## Per-branch state, inputs and outputs
6565

66-
Each branch runs its own subgraph against its own state heterogeneous
66+
Each branch runs its own subgraph against its own state; heterogeneous
6767
schemas are explicit. Subgraph fields named in `inputs` are seeded
6868
from the parent's corresponding field at branch entry; other subgraph
6969
fields take their schema defaults. At branch exit, only the parent
@@ -72,7 +72,7 @@ branch's final state is discarded.
7272

7373
When two branches contribute to the same parent field, the parent's
7474
reducer for that field applies both values in **branch insertion
75-
order** first the branch declared first in the `branches` dict,
75+
order**: first the branch declared first in the `branches` dict,
7676
then the next, and so on. This is deterministic regardless of which
7777
branch's inner work finishes first.
7878

@@ -83,7 +83,7 @@ branch's inner work finishes first.
8383
`ParallelBranchesBranchFailed` (a `NodeException` subtype) carrying
8484
the failing `branch_name` and the original cause as `__cause__`.
8585
`recoverable_state` is the parent's snapshot at the moment the
86-
dispatcher entered**no buffered branch contributions are
86+
dispatcher entered. **No buffered branch contributions are
8787
applied**, including those of branches that successfully completed
8888
before the failure. Buffer-and-apply semantics: contributions are
8989
held until every branch finishes, then either all apply (success)
@@ -100,7 +100,7 @@ branch's inner work finishes first.
100100

101101
## Branch middleware
102102

103-
Each `BranchSpec` accepts a `middleware` tuple middlewares that
103+
Each `BranchSpec` accepts a `middleware` tuple of middlewares that
104104
wrap that branch's whole subgraph invocation as a unit. Retry
105105
middleware on a branch retries the **whole branch**: a fresh
106106
subgraph invocation each time, fresh inner-node execution. The
@@ -109,7 +109,7 @@ inner nodes (per graph-engine §6 v0.16.1), so observer events
109109
inside the branch correctly show `attempt_index` ticking across
110110
retries.
111111

112-
Branch middleware is independent across branches branch A may
112+
Branch middleware is independent across branches: branch A may
113113
have `[retry, timing]`; branch B may have `[]`; branch C may have
114114
some custom breaker. Each branch's chain composes in isolation.
115115

@@ -118,15 +118,15 @@ some custom breaker. Each branch's chain composes in isolation.
118118
Parallel branches compose with the rest of the engine the way
119119
subgraphs and fan-outs do:
120120

121-
- A branch's subgraph can itself contain a fan-out node inner-node
121+
- A branch's subgraph can itself contain a fan-out node; inner-node
122122
events inside that fan-out carry **both** `branch_name` (this
123123
branch) and `fan_out_index` (the instance within this branch).
124124
The two fields are independent.
125125
- The parallel-branches node itself can be invoked from inside a
126-
fan-out instance inner events then carry the outer fan-out's
126+
fan-out instance, and inner events then carry the outer fan-out's
127127
`fan_out_index` and the inner branch's `branch_name`.
128128
- Per-graph and per-node middleware on the parallel-branches node
129-
wrap the dispatcher as a single unit one `started` event before
129+
wrap the dispatcher as a single unit: one `started` event before
130130
dispatch begins, one `completed` event after all branches finish
131131
and fan-in lands. The parent's retry middleware retries the **whole
132132
parallel-branches node**, not individual branches.
@@ -143,9 +143,9 @@ Per-branch progress is not individually persisted in v1.
143143
- **Not the same as N copies of one subgraph.** If you want "run
144144
this subgraph for each item in a list," reach for
145145
[fan-out](fan-out.md).
146-
- **Not a router.** A router is a conditional-edge pattern — pick
147-
one branch based on state. Parallel branches runs *all* branches
148-
concurrently.
146+
- **Not a router.** A router is a conditional-edge pattern that
147+
picks one branch based on state. Parallel branches runs *all*
148+
branches concurrently.
149149
- **Not a coordinator.** Branches don't communicate with each other
150150
during execution; if branch B's work depends on branch A's
151151
output, you want a linear pipeline (A → B), not parallel branches.

docs/getting-started/index.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ assert final.log == ["hello", "world"]
6969

7070
## Next
7171

72-
- [Concepts](../concepts/index.md): deeper on state, reducers,
73-
projections, fan-out, subgraphs, observability.
74-
- [Examples](https://github.com/LunarCommand/openarmature-python/tree/main/examples):
75-
five runnable demos, each driving a local OpenAI-compatible LLM
76-
endpoint to do real work.
72+
- [Concepts](../concepts/index.md): deeper on state, reducers, graphs,
73+
composition, fan-out, parallel branches, LLMs, prompts,
74+
observability, checkpointing.
75+
- [Examples](../examples/index.md): ten runnable demos with
76+
walk-throughs, each driving an OpenAI-compatible LLM endpoint to
77+
do real work.
7778
- [API reference](../reference/index.md): auto-generated from docstrings.

0 commit comments

Comments
 (0)