Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions conformance.toml
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,12 @@ status = "textual-only"
since = "0.9.0"
note = "Drain snapshot semantic and timeout-input validation already implemented as part of the proposal 0010 impl PR (v0.9.0); no additional module-level work needed."

# Spec v0.23.0-v0.26.0 batch (proposals 0031, 0032, 0033, 0034). All
# four have impl work landing across the v0.10.0 release cycle; status
# stays `not-yet` until the release PR flips them to `implemented`
# with `since = "0.10.0"`. The pinned spec submodule advances ahead
# of the impl status because newer fixtures need to be visible to
# the conformance harness as each PR lands.
# Spec v0.23.0-v0.26.1 batch (proposals 0031, 0032, 0033, 0034, 0035).
# All five have impl work landing across the v0.10.0 release cycle;
# status stays `not-yet` until the release PR flips them to
# `implemented` with `since = "0.10.0"`. The pinned spec submodule
# advances ahead of the impl status because newer fixtures need to be
# visible to the conformance harness as each PR lands.
[proposals."0031"]
status = "not-yet"

Expand All @@ -167,3 +167,6 @@ status = "not-yet"

[proposals."0034"]
status = "not-yet"

[proposals."0035"]
status = "not-yet"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Specification = "https://github.com/LunarCommand/openarmature-spec"
openarmature = "openarmature.cli:main"

[tool.openarmature]
spec_version = "0.26.0"
spec_version = "0.26.1"

[dependency-groups]
dev = [
Expand Down
4 changes: 2 additions & 2 deletions src/openarmature/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenArmature — Agent documentation

*This is the agent guide bundled with the openarmature Python package, version 0.9.0 (spec v0.26.0). For the full docs site see [openarmature.ai](https://openarmature.ai). For the canonical spec text see [openarmature.org/capabilities](https://openarmature.org/capabilities/). For project-specific conventions for the code you're editing, see the host project's `AGENTS.md` or `CLAUDE.md`.*
*This is the agent guide bundled with the openarmature Python package, version 0.9.0 (spec v0.26.1). For the full docs site see [openarmature.ai](https://openarmature.ai). For the canonical spec text see [openarmature.org/capabilities](https://openarmature.org/capabilities/). For project-specific conventions for the code you're editing, see the host project's `AGENTS.md` or `CLAUDE.md`.*

## TL;DR

Expand All @@ -10,7 +10,7 @@ OpenArmature is a workflow framework for LLM pipelines and tool-calling agents

## Capability contracts

_Sourced from openarmature-spec v0.26.0. Each entry below reproduces §1 (Purpose) and §2 (Concepts) of the capability's `spec.md`. For the full spec text (execution model, error semantics, determinism, observer hooks, etc.) see the linked docs site._
_Sourced from openarmature-spec v0.26.1. Each entry below reproduces §1 (Purpose) and §2 (Concepts) of the capability's `spec.md`. For the full spec text (execution model, error semantics, determinism, observer hooks, etc.) see the linked docs site._

### Capability: `graph-engine`

Expand Down
2 changes: 1 addition & 1 deletion src/openarmature/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
"""

__version__ = "0.9.0"
__spec_version__ = "0.26.0"
__spec_version__ = "0.26.1"
2 changes: 2 additions & 0 deletions src/openarmature/graph/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def add_fan_out_node[ChildT: State](
instance_middleware: Iterable[Middleware] | None = None,
errors_field: str | None = None,
middleware: Iterable[Middleware] | None = None,
subgraph_identity: str | None = None,
) -> Self:
"""Register a fan-out node.

Expand Down Expand Up @@ -262,6 +263,7 @@ def add_fan_out_node[ChildT: State](
extra_outputs=dict(extra_outputs or {}),
instance_middleware=tuple(instance_middleware or ()),
errors_field=errors_field,
subgraph_identity=subgraph_identity,
)
# FanOutNode satisfies the Node[StateT] structural protocol (run
# returns a partial update; name and middleware are present),
Expand Down
3 changes: 3 additions & 0 deletions src/openarmature/graph/compiled.py
Original file line number Diff line number Diff line change
Expand Up @@ -1987,6 +1987,7 @@ def _dispatch_started(
fan_out_index=context.fan_out_index,
fan_out_config=fan_out_config,
branch_name=current_branch_name(),
subgraph_identities=context.subgraph_identities,
),
)

Expand Down Expand Up @@ -2020,6 +2021,7 @@ def _dispatch_completed(
fan_out_index=context.fan_out_index,
fan_out_config=fan_out_config,
branch_name=current_branch_name(),
subgraph_identities=context.subgraph_identities,
),
)

Expand Down Expand Up @@ -2202,5 +2204,6 @@ async def _maybe_save_checkpoint(
parent_states=context.parent_states_prefix,
attempt_index=attempt_index,
fan_out_index=None,
subgraph_identities=context.subgraph_identities,
),
)
14 changes: 14 additions & 0 deletions src/openarmature/graph/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,20 @@ class NodeEvent:
# simultaneously when a branch's subgraph contains a fan-out
# (and vice versa).
branch_name: str | None = None
# Per observability §5.3 + the coord-thread
# ``clarify-subgraph-name-semantics`` resolution: chain of
# compiled-subgraph identities parallel to the wrapper-depth
# positions of ``namespace``. Index ``i`` is the identity for
# the wrapper at ``namespace[i]`` (or ``None`` when that
# wrapper has no tracked identity); chain length equals the
# depth of wrapper nesting (always ``< len(namespace)`` since
# the last element of ``namespace`` is the current node, not
# a wrapper). Observers read by depth and emit it as
# ``observation.metadata.subgraph_name`` (Langfuse) /
# ``openarmature.subgraph.name`` (OTel), falling back to the
# empty string when ``None`` per §5.3's "if the implementation
# tracks one" clause.
subgraph_identities: tuple[str | None, ...] = ()


__all__ = ["FanOutEventConfig", "NodeEvent"]
10 changes: 10 additions & 0 deletions src/openarmature/graph/fan_out.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ class FanOutConfig:
extra_outputs: Mapping[str, str] = field(default_factory=dict[str, str])
instance_middleware: tuple[Middleware, ...] = ()
errors_field: str | None = None
# The identity of the compiled inner subgraph (the key under
# which the subgraph is declared in a ``subgraphs:`` registry).
# Threaded onto every per-instance event so observers can emit
# ``observation.metadata.subgraph_name`` on each per-instance
# dispatch observation (Langfuse) /
# ``openarmature.subgraph.name`` on the corresponding span
# (OTel). Optional and BC-preserving — direct callers that don't
# supply it get the empty-string fallback per observability §5.3.
subgraph_identity: str | None = None


@dataclass(frozen=True)
Expand Down Expand Up @@ -271,6 +280,7 @@ async def run_instance(idx: int, instance_state: ChildT) -> Mapping[str, Any]:
parent_state=state,
sub_attached=tuple(cfg.subgraph._attached_observers), # noqa: SLF001
fan_out_index=idx,
subgraph_identity=cfg.subgraph_identity,
)

async def innermost(s: ChildT) -> Mapping[str, Any]:
Expand Down
24 changes: 24 additions & 0 deletions src/openarmature/graph/observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,18 @@ class _InvocationContext:
step_counter: list[int] = field(default_factory=lambda: [0])
namespace_prefix: tuple[str, ...] = ()
parent_states_prefix: tuple[State, ...] = ()
# Per observability §5.3 + the coord-thread `clarify-subgraph-name-
# semantics` resolution. Parallel to ``namespace_prefix`` — index
# ``i`` is the compiled-subgraph identity for the wrapper at
# ``namespace_prefix[i]``, or ``None`` for wrappers constructed
# without an identity. Used by observers to emit
# ``metadata.subgraph_name`` (Langfuse) and
# ``openarmature.subgraph.name`` (OTel) on the wrapper observation
# / span at each depth. The chain shape lets nested subgraphs
# carry distinct identities at distinct depths even though
# v0.10.0's conformance fixtures only exercise single-level
# nesting.
subgraph_identities: tuple[str | None, ...] = ()
# Per pipeline-utilities §9 + graph-engine §6: nodes inside a
# fan-out instance fire events tagged with the instance's 0-based
# index. Set when descending into a fan-out instance, inherited
Expand Down Expand Up @@ -426,6 +438,8 @@ def descend_into_subgraph(
subgraph_node_name: str,
parent_state: State,
sub_attached: tuple[SubscribedObserver, ...],
*,
subgraph_identity: str | None = None,
) -> _InvocationContext:
"""Build the context for a subgraph-as-node call.

Expand All @@ -447,6 +461,7 @@ def descend_into_subgraph(
step_counter=self.step_counter,
namespace_prefix=self.namespace_prefix + (subgraph_node_name,),
parent_states_prefix=self.parent_states_prefix + (parent_state,),
subgraph_identities=self.subgraph_identities + (subgraph_identity,),
fan_out_index=self.fan_out_index,
invocation_id=self.invocation_id,
correlation_id=self.correlation_id,
Expand All @@ -466,6 +481,8 @@ def descend_into_fan_out_instance(
parent_state: State,
sub_attached: tuple[SubscribedObserver, ...],
fan_out_index: int,
*,
subgraph_identity: str | None = None,
) -> _InvocationContext:
"""Build the context for one fan-out instance's subgraph invocation.

Expand All @@ -491,6 +508,7 @@ def descend_into_fan_out_instance(
step_counter=self.step_counter,
namespace_prefix=self.namespace_prefix + (fan_out_node_name,),
parent_states_prefix=self.parent_states_prefix + (parent_state,),
subgraph_identities=self.subgraph_identities + (subgraph_identity,),
fan_out_index=fan_out_index,
invocation_id=self.invocation_id,
correlation_id=self.correlation_id,
Expand Down Expand Up @@ -541,6 +559,12 @@ def descend_into_parallel_branch(
step_counter=self.step_counter,
namespace_prefix=self.namespace_prefix + (parallel_branches_node_name,),
parent_states_prefix=self.parent_states_prefix + (parent_state,),
# Parallel-branches don't reify a single inner subgraph
# identity at the wrapper position — each branch can hold a
# different subgraph — so we extend the chain with ``None``
# at this depth. Per-branch identity handling (if ever
# needed) is a future addition.
subgraph_identities=self.subgraph_identities + (None,),
fan_out_index=self.fan_out_index,
invocation_id=self.invocation_id,
correlation_id=self.correlation_id,
Expand Down
12 changes: 12 additions & 0 deletions src/openarmature/graph/subgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ class SubgraphNode[ParentT: State, ChildT: State]:
default_factory=FieldNameMatching[ParentT, ChildT]
)
middleware: tuple[Middleware, ...] = field(default_factory=tuple[Middleware, ...])
# The compiled subgraph's identity (the registry key under which
# the subgraph is declared, distinct from the wrapper node's
# ``name`` in the parent graph). Optional and BC-preserving:
# callers that don't pass it get an empty string emitted as the
# observability §5.3 ``subgraph_name`` attribute (matching the
# spec's "if the implementation tracks one" fallback). Setting
# it lets dashboards filter or aggregate across observations from
# the same compiled subgraph wrapped under different node names
# (e.g., a ``validator`` subgraph used as both ``validate_input``
# and ``validate_output``).
subgraph_identity: str | None = None

async def run(
self,
Expand Down Expand Up @@ -116,6 +127,7 @@ async def run(
subgraph_node_name=self.name,
parent_state=state,
sub_attached=tuple(self.compiled._attached_observers),
subgraph_identity=self.subgraph_identity,
)
sub_final = await self.compiled._invoke(sub_initial, child_context)
return self.projection.project_out(sub_final, state, self.compiled.state_cls)
Loading