@tool – Dual‑mode Tool Decorator¶
The @tool decorator turns a plain Python callable into an AetherGraph tool:
- Immediate mode (no active graph): calling the decorated function executes it directly and returns a dict of outputs (or an awaitable for async tools).
- Graph mode (inside
@graph_fn/@graphify): calling the decorated function builds a node and returns aNodeHandle; nothing is executed yet. - Registry integration: when a registry is active, the implementation is automatically registered under the
toolnamespace. - Context injection: if the tool signature includes a
contextparameter, aNodeContextis injected automatically at run time when the tool runs as a node in a graph.
Signature¶
@tool(
outputs: list[str],
inputs: list[str] | None = None,
*,
name: str | None = None,
version: str = "0.1.0",
)
Required vs optional¶
| Parameter | Type | Required? | Description |
|---|---|---|---|
outputs |
list[str] |
Yes | Declares the named outputs produced by the tool. Every call must return a dict containing exactly these keys. |
inputs |
list[str] \| None |
No | Optional explicit input names. If None, they are inferred from the implementation’s signature (excluding *args, **kwargs). |
name |
str \| None (keyword‑only) |
No | Optional registry / UI name. Defaults to the underlying implementation’s __name__. |
version |
str (keyword‑only) |
No | Semantic version used for registry and provenance. Defaults to "0.1.0". |
Notes
outputsis always required and defines the contract enforced at runtime.inputsis usually optional – in most cases, you can let AetherGraph infer it from the function’s parameters.nameandversionmatter when you later want to look up tools in a registry or inspect runs.
Behavior Overview¶
-
Sync vs async
-
If the implementation is synchronous, the decorated function returns a dict in immediate mode.
-
If the implementation is async, the decorated function returns an awaitable in immediate mode.
-
Graph vs immediate mode
-
When a graph builder is active (
current_builder() is not None), calling the tool returns aNodeHandle(graph node), not data. -
When no builder is active, calling the tool executes the implementation and returns results.
-
Context injection
-
If the tool’s signature includes a parameter named
context, it is treated as a reserved injectable rather than a normal data input. - You do not list
"context"in theinputslist;inputsis for data‑flow arguments only. - When the tool runs as a node in a graph (via
@graph_fnorgraphify), AetherGraph automatically injects aNodeContextinstance for that node. Callers do not pass it manually when running the graph.
Runtime Contracts¶
The decorator enforces a strict I/O contract for every tool call:
-
The implementation must return either:
-
a
dictwith exactly the declaredoutputskeys, or -
a value that
_normalize_resultcan convert into such a dict. -
_check_contract(outputs, out, impl)validates that: -
all
outputskeys are present, - no unexpected keys are produced (where applicable).
On violation, the runtime raises a clear error pointing at the original implementation.
Context‑aware Tools¶
To use runtime services inside a tool (channel, memory, artifacts, logger, etc.), add a context parameter:
from aethergraph import tool, NodeContext
@tool(outputs=["y"], inputs=["x"])
async def double_with_log(x: int, *, context: NodeContext) -> dict:
# `context` is injected automatically when this tool runs as a graph node
context.logger().info("doubling", extra={"x": x})
return {"y": x * 2}
Contracts:
contextis not part of the data‑flow; it never becomes an edge in the graph.- In graph mode, callers simply write
double_with_log(x=42)in a@graph_fn/graphifybody; the runner injectsNodeContextwhen executing the node. - In immediate mode (outside any graph), you can optionally pass a
contextargument manually if you want to test the tool with a synthetic context, but typical usage is inside graphs.
Usage Patterns¶
Below are common ways to define and call tools. These examples focus on shape and contracts; see the main Graph docs for deeper patterns.
1. Simple synchronous tool
from aethergraph import tool
@tool(outputs=["y"])
def double(x: int) -> dict:
return {"y": x * 2}
# Immediate mode (no active graph)
result = double(x=21)
assert result == {"y": 42}
Key points
outputs=["y"]is required.inputsis omitted → inferred as["x"]from the function signature.- In immediate mode,
double(...)runs immediately and returns a dict.
2. Async tool
from aethergraph import tool
@tool(outputs=["text"])
async def fetch_text(url: str) -> dict:
data = await some_async_http_get(url)
return {"text": data}
# Immediate mode
result = await fetch_text(url="https://example.com")
print(result["text"])
Key points
- Implementation is async → in immediate mode, calling
fetch_text(...)returns an awaitable. inputsagain inferred from the signature (["url"]).
3. Explicit inputs (with non‑data params)
from aethergraph import tool, NodeContext
@tool(outputs=["out"], inputs=["a", "b"])
async def add(a: int, b: int = 0, *, scale: int = 1, context: NodeContext | None = None) -> dict:
# `a`, `b` are data inputs; `scale` and `context` are not graph edges
if context is not None:
context.logger().info("adding", extra={"a": a, "b": b, "scale": scale})
return {"out": (a + b) * scale}
Key points
inputs=["a", "b"]means onlyaandbare considered data‑inputs from upstream nodes.-
scaleandcontextare non‑data parameters: -
contextis a reserved injectable (runtime will supply it in graph mode). scalecan be passed as a literal or configured inside the graph; it does not appear as an edge unless you model it explicitly.
4. Graph mode – building nodes
from aethergraph import graph_fn, tool, NodeContext
@tool(outputs=["y"])
def double(x: int):
return {"y": x * 2}
@tool(outputs=["y"], inputs=["x"])
async def double_with_log(x: int, *, context: NodeContext) -> dict:
context.logger().info("doubling", extra={"x": x})
return {"y": x * 2}
@graph_fn(name="pipeline", outputs=["z"])
async def pipeline(*, context: NodeContext):
# Inside a graph_fn, calling tools builds nodes
n1 = double(x=21) # NodeHandle, not the numeric 42
n2 = double_with_log(x=n1.y) # NodeHandle with NodeContext injection at run time
return {"z": n2.y}
Key points
- Inside
@graph_fn, bothdouble(...)anddouble_with_log(...)do not run immediately. - Instead,
NodeHandles are returned and the builder records tool nodes and their dependencies. -
When the graph is executed:
-
The runtime calls the underlying implementation with concrete values for data inputs.
- If a
contextparameter is present, aNodeContextinstance is injected automatically for that node.
Registry Behavior (Advanced)¶
When a registry is active (current_registry() is not None), the decorator automatically registers the underlying implementation:
registry.register(
nspace="tool",
name=name or impl.__name__,
version=version,
obj=impl,
)
This enables:
- Discovery – listing available tools by
(name, version). - Provenance – runs can record which version of which tool was used.
- Hot reload / development – registries can swap implementations without changing graph code.
If no registry is active, the decorator still works normally; registration is simply skipped.
Summary¶
- Use
@tool(outputs=[...])on any function to make it part of the graph runtime. - Required:
outputs– define the contract. - Optional:
inputs,name,version– control graph wiring and registry metadata. - Immediate calls return concrete data (or awaitables); calls inside graphs create nodes and wire dependencies.
-
To use runtime services inside a tool, add a
contextparameter: -
It is automatically injected as
NodeContextwhen the tool runs as a node in a graph. - Callers never pass
contextmanually when running@graph_fnorgraphifygraphs; the runner does it for them.