Skip to content

Commit f06ad62

Browse files
Bump spec to v0.20.1; refresh docs and CHANGELOG
Submodule pin advances from v0.19.0 to v0.20.1, absorbing proposal 0025 (tool_choice, v0.20.0) and proposal 0026 (§8.X wire-format mapping subsection template, v0.20.1). 0026 is purely textual; the existing OpenAI §8.1 mapping is the template's reference shape so no python module-level work is needed. Runtime spec_version pins in pyproject and __init__ updated to match; smoke test asserts v0.20.1. CHANGELOG Unreleased section gains tool_choice + ForceTool + ToolChoice + validate_tool_choice entries under Added; the Provider.complete() signature extension noted under Changed; cumulative pin-bump summary updated to v0.17.0 -> v0.20.1 across six spec versions absorbed this cycle. docs/concepts/llms.md gains a "Controlling tool-call behavior with tool_choice" subsection under Tool calling, covering the four modes, the three pre-send validation rules, and the cross-provider caveat (not all providers honor tool_choice).
1 parent a0c6f00 commit f06ad62

6 files changed

Lines changed: 50 additions & 5 deletions

File tree

CHANGELOG.md

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

99
### Added
1010

11+
- **`tool_choice` parameter on `Provider.complete()`** (proposal 0025, accepted in spec v0.20.0). Optional discriminated-union value constraining the model's tool-calling behavior — one of `"auto"`, `"required"`, `"none"`, or a `ForceTool(name=...)` record. Validation runs pre-send: `"required"` and `ForceTool` both demand non-empty `tools`, and `ForceTool.name` must appear in the supplied list; violations raise `ProviderInvalidRequest` (§7's existing category — no new error category). When `tool_choice` is `None` (the default) the wire field is omitted and the provider's own default applies, preserving pre-0025 behavior exactly. The `OpenAIProvider` maps the spec shape onto OpenAI's wire shape per §8.1.1 (the `ForceTool.type="tool"` renames to wire `type="function"`).
12+
- **`ForceTool` and `ToolChoice` public types** at `openarmature.llm.ForceTool` / `openarmature.llm.ToolChoice`. `ForceTool` is a frozen Pydantic model with `type: Literal["tool"] = "tool"` and `name: str`; `ToolChoice = Literal["auto", "required", "none"] | ForceTool` is the type alias used in `Provider.complete()`'s signature.
13+
- **`validate_tool_choice` public validator** at `openarmature.llm.validate_tool_choice`. Standalone validator covering the three §5 pre-send rules; useful for third-party `Provider` implementations that want to reuse the canonical validation logic.
1114
- **Bounded drain timeout on `CompiledGraph.drain()`** (proposal 0010, accepted in spec v0.19.0). `drain()` accepts an optional `timeout: float | None = None` parameter (non-negative seconds). When supplied, drain returns no later than the deadline; any observer events still queued or in-flight are reported as undelivered. Workers are cancelled cleanly so the compiled graph remains usable for subsequent invocations — partial delivery state from one drain does NOT leak into the next. Solves the "slow / hung / misbehaving observer blocks process exit" footgun for short-lived processes (CLIs, scripts, serverless functions). Observers SHOULD be cancellation-safe (idempotent writes, `try/finally` cleanup); the spec doesn't mandate it but the docs recommend it.
1215
- **`DrainSummary` frozen dataclass** at `openarmature.graph.DrainSummary`. Returned from every `drain()` call (with or without `timeout`). Fields: `undelivered_count: int`, `timeout_reached: bool`. The shape is consistent across timed and untimed drains — callers receive the same dataclass whether the timeout was supplied or not. Per the v0.19.0 contract the two declared fields are the spec-mandated minimum; richer diagnostic detail (per-observer counts, sampled event metadata) is reserved for follow-on PRs.
1316
- **Per-instance fan-out resume contract** (proposal 0009, accepted in spec v0.18.0). The engine now writes a checkpoint record at every `completed` event inside a fan-out instance (in addition to the existing outermost-graph + subgraph-internal + fan-out node completion saves). On resume the engine consults the saved record's `fan_out_progress` field and treats each instance as `completed` (skip, contribution rolls forward), `in_flight` (re-run from subgraph entry), or `not_started` (dispatch normally). The `append` reducer's no-double-merge guarantee holds across resume because `completed` is a one-shot accumulator state.
@@ -17,13 +20,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
1720

1821
### Changed
1922

23+
- **`Provider.complete()` signature** extended with an optional `tool_choice: ToolChoice | None = None` parameter (per proposal 0025 v0.20.0). Backward-compatible: callers that omit the new argument see no wire-shape change. Third-party `Provider` implementations that don't add the parameter still structurally satisfy the `Provider` Protocol; their wire path silently drops `tool_choice`. The OpenAI mapping is in `OpenAIProvider`; future Anthropic / Gemini providers will follow the §8.X template (proposal 0026) and ship their own `tool_choice` wire mapping.
2024
- **`CompiledGraph.drain()` return type** changed from `None` to `DrainSummary` (pre-1.0; per proposal 0010 v0.19.0 contract). Callers that ignored the return are unaffected — `await graph.drain()` discards the returned dataclass exactly as before. Callers that explicitly typed the return as `None` will need to update their annotation.
2125
- **Fan-out resume behavior** flipped from atomic restart (0008's v1 contract) to per-instance resume. A crash mid-fan-out used to re-run the entire fan-out on resume; now only the instances that did not complete-and-record their contribution re-run. The economics matter for large fan-outs of expensive work (LLM calls, long extractions): an 80% complete fan-out crash now restores 80% of its results rather than discarding them.
2226
- **`SQLiteCheckpointer` schema** picks up a new `fan_out_progress_blob` column (added via `ALTER TABLE` for backward compatibility with pre-0009 databases). Pre-0009 rows back-fill as NULL on load and round-trip as the empty-tuple default. Both `pickle` and `json` serialization modes round-trip the new field.
2327

2428
### Notes
2529

26-
- **Pinned spec version bumped from v0.17.0 to v0.19.0 over this Unreleased cycle.** Four spec versions absorbed: v0.17.1 (proposal 0019, multi-provider wire-format extension; purely textual reframe of llm-provider §8 as a catalog of wire-format mappings, OpenAI-compatible body nested under §8.1, code references updated to §8.1 / §8.1.1 / §8.1.2 / §8.1.3 / §8.1.5.1 / §8.1.1.1), v0.18.0 (proposal 0009, per-instance fan-out resume; pipeline-utilities §10.3 / §10.7 revised, §10.11 added with per-instance state machine plus composition rules plus configurable batching; the `append` reducer no-double-merge invariant from §10.11.1 is the load-bearing correctness story; see Added / Changed above), v0.18.1 (fixture-only patch on `release/v0.18.1` correcting an off-by-one literal in fixture 052's expected `results`), and v0.19.0 (proposal 0010, bounded drain timeout; graph-engine §6 amended with the `timeout` parameter and `DrainSummary` return contract; see Added / Changed above). All existing conformance fixtures continue to pass.
30+
- **Pinned spec version bumped from v0.17.0 to v0.20.1 over this Unreleased cycle.** Six spec versions absorbed: v0.17.1 (proposal 0019, multi-provider wire-format extension — purely textual reframe of llm-provider §8 as a catalog of wire-format mappings; OpenAI-compatible body nested under §8.1), v0.18.0 (proposal 0009, per-instance fan-out resume — pipeline-utilities §10.3 / §10.7 revised, §10.11 added; the `append` reducer no-double-merge invariant is the load-bearing correctness story), v0.18.1 (fixture-only patch correcting an off-by-one literal in fixture 052's expected `results`), v0.19.0 (proposal 0010, bounded drain timeout — graph-engine §6 amended with the `timeout` parameter and `DrainSummary` return contract), v0.20.0 (proposal 0025, llm-provider `tool_choice` — §5 / §7 / §8.1.1 amended; see Added / Changed above), and v0.20.1 (proposal 0026, llm-provider §8.X wire-format mapping subsection template — purely textual §8 framing paragraph; the existing OpenAI §8.1 mapping is the template's reference shape so no python module-level work was needed). All existing conformance fixtures continue to pass.
2731

2832
## [0.8.0] — 2026-05-23
2933

docs/concepts/llms.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,47 @@ prevents runaway loops on a model that stays in tool-calling forever.
273273
See [`09 - Tool use`](../examples/09-tool-use.md) for the runnable
274274
shape.
275275

276+
### Controlling tool-call behavior with `tool_choice`
277+
278+
By default the model decides whether and which tools to call.
279+
`tool_choice` constrains that decision per call. Four modes:
280+
281+
- `"auto"` — the model decides. Equivalent to omitting the parameter
282+
when `tools` is non-empty.
283+
- `"required"` — the model MUST call at least one tool. Useful for
284+
routing nodes that branch on tool selection.
285+
- `"none"` — the model MUST NOT call tools, even if `tools` is
286+
supplied. Useful for guarded LLM calls or for explicitly disabling
287+
tool-calling without rebuilding a tools-less request.
288+
- `ForceTool(name=...)` — the model MUST call the named tool exactly.
289+
290+
Pre-send validation catches the three failure modes (`required` with
291+
empty tools, `ForceTool` with empty tools, `ForceTool.name` not in
292+
the supplied list) and raises `ProviderInvalidRequest` before the
293+
HTTP request is sent.
294+
295+
```python
296+
from openarmature.llm import ForceTool
297+
298+
# Routing node: model MUST pick one of the supplied tools.
299+
response = await provider.complete(
300+
messages, tools=[search, summarize], tool_choice="required"
301+
)
302+
303+
# Forced specific tool: useful when the pipeline knows which tool
304+
# the model should call next (e.g., a `dispatch_search` node).
305+
response = await provider.complete(
306+
messages, tools=[search, summarize], tool_choice=ForceTool(name="search")
307+
)
308+
```
309+
310+
Not all providers honor `tool_choice` — confirm with your provider's
311+
documentation. The `OpenAIProvider` maps the spec shape onto OpenAI's
312+
wire shape per the §8.1.1 mapping table. Whether the model actually
313+
honored the constraint is observable from the returned
314+
`finish_reason` and `tool_calls` fields; the framework does NOT
315+
re-validate the response against the constraint.
316+
276317
## Content blocks (multimodal user messages)
277318

278319
User messages carry content in one of two shapes: a plain text string,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Repository = "https://github.com/LunarCommand/openarmature-python"
4848
Specification = "https://github.com/LunarCommand/openarmature-spec"
4949

5050
[tool.openarmature]
51-
spec_version = "0.19.0"
51+
spec_version = "0.20.1"
5252

5353
[dependency-groups]
5454
dev = [

src/openarmature/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""OpenArmature: workflow framework for LLM pipelines and tool-calling agents."""
22

33
__version__ = "0.8.0"
4-
__spec_version__ = "0.19.0"
4+
__spec_version__ = "0.20.1"

tests/test_smoke.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
def test_package_versions() -> None:
1111
assert openarmature.__version__ == "0.8.0"
12-
assert openarmature.__spec_version__ == "0.19.0"
12+
assert openarmature.__spec_version__ == "0.20.1"
1313

1414

1515
def test_spec_version_matches_pyproject() -> None:

0 commit comments

Comments
 (0)