operonx.core.ops¶
Op base classes, flow primitives, and graph composition.
ops
¶
Core op types and markers for the Operon workflow engine.
BaseOp— base class for all workflow opsDummyOp— placeholder for START/END markersGraphOp— container managing a sub-graph with parallel executionBranchOp— conditional routing with precompiled conditionsFuncOp— wraps a Python function (supports generators for streaming)
Text→structured-output parsing is done inline by LLMOp(fields=..., parser=...,
validators=...) — the standalone ParserOp was removed in 1.0.0. Its
pure functions live in operonx.providers.parsing for callers that need
text parsing without an LLM call.
Markers: START, END, PARENT, PENDING.
Decorators: op, graph, if_.
Attributes¶
OpType
module-attribute
¶
OpType = Literal[
"data",
"llm",
"embedding",
"rerank",
"vector-search",
"vector-upsert",
"vector-delete",
"doc-fetch",
"branch",
"interrupt",
"emit",
"code",
"lambda",
"prompt",
"doc-processor",
"graph",
"default",
"dummy",
"tool-executor",
"mcp",
"agent",
]
Các loại node được hỗ trợ trong workflow graph.
Removed in 1.2.0:
for / while / stream — superseded in 1.0.0 by back-edge
loops, generator ops and Ref.parallel().
parser — ParserOp went away in 1.0.0; parsing lives inside
LLMOp(fields=...).
milvus / mongo / s3 — named backends, not semantics, and
never had ops behind them. Storage is reached through
vector-search and doc-fetch.
onnx / triton — assigned by the ops deleted in 1.2.0 (they
were never in this Literal, which is the drift this cleanup ends).
Added after 1.14.0:
vector-upsert / vector-delete — VectorUpsertOp /
VectorDeleteOp, the write half of vector-search.
agent — operonx_agents.AgentOp (Agent.as_op()): a whole
agent run, its turns, model and tool calls child executions.
Added in 1.2.0:
interrupt / emit — set by InterruptOp / EmitOp since
1.0.0 but missing here, so the Literal disagreed with the code.
Classes¶
EOF
dataclass
¶
Marker that an op's async generator has exhausted.
Created by Scheduler._pump() after op.run() stops yielding
(i.e. the underlying function returned or its generator was exhausted).
User code never yields EOF — it is emitted implicitly when the op finishes.
Frame
dataclass
¶
One result yielded by an op during execution.
Created by Scheduler._pump() for every (ctx, result) tuple that
op.run() yields. User code never constructs Frame directly — just
return or yield from an @op function.
Interrupt
dataclass
¶
In-band scheduler cancellation event.
Returned/yielded by user op bodies to cancel queued frames + in-flight
tasks at ctx_to_cancel (and its descendants). op and ctx
record the emitter for tracing; ctx_to_cancel is the target —
typically the prior turn's ctx, stored in SCRATCH when long-running
work began.
ctx_to_cancel defaults to :data:Interrupt.SELF, which the
scheduler resolves to the emitter's own context. Cancelling the whole
run is spelled Interrupt.ALL and has to be asked for::
Interrupt(reason="bad input") # this branch
Interrupt(ctx_to_cancel=Interrupt.ALL, ...) # everything
The empty tuple used to be the default, and it is a prefix of every context — so omitting the argument discarded the entire run and returned cleanly, with no error anywhere for the caller to notice.
The scheduler
- Drops Frame/EOF items at ctx_to_cancel from the queue.
- Cancels in-flight
_pumptasks at ctx_to_cancel and descendants (skipping the emitter to avoid self-cancel). - Clears bookkeeping (ready/seq_active/seq_origins/collect_bufs).
- Forwards a synthetic
("__interrupt__", emitter_ctx, {...})tuple tooutput_queuesoExecutionHandleconsumers see it.
Best-effort: data already pushed to consumer-owned queues (e.g. a
user-supplied asyncio.Queue) is NOT drained — consumer must handle
that itself (see plan §4.6a).
BaseOp
¶
BaseOp(
id: str = None,
name: str = None,
description: str = "",
inputs: Dict[str, Any] = None,
outputs: Dict[str, Any] = None,
sources: List[str] = None,
targets: List[str] = None,
stream: bool = False,
start: bool = False,
end: bool = False,
contain_generation: bool = False,
verbose: bool = True,
enabled: bool = True,
bound: Optional[str] = None,
cache: Union[bool, str, None] = None,
delay: float = 0,
exclude=None,
include=None,
observe_max: Optional[int] = None,
transient: bool = False,
show_keys: Union[str, Sequence[str], None] = None,
name_hint: Optional[str] = None,
door: Optional[str] = None,
retry=None,
timeout=None,
)
Bases: ABC
Base class for all ops in a workflow.
An op is the fundamental processing unit. Each op declares typed inputs
and outputs (via Param), and implements a core() method that
contains the execution logic. Ops are wired together inside a GraphOp
using edge operators.
Sections (read top-to-bottom)::
1. INIT __init__, __slots__, param helpers
2. EDGE OPERATORS >>, >>~, >, <, [], ~ — wiring ops in a graph
3. EXECUTE run(), get_inputs/outputs, store_result, _exec_core
4. OBSERVABILITY _log(), _store_metrics()
5. SERIALIZATION serialize(), metadata — config dict & tracing
Example::
from operonx.core import GraphOp, op, START, END, PARENT
@op
def double(x: int):
return {"result": x * 2}
with GraphOp(name="main") as graph:
d = double(x=PARENT["x"])
START >> d >> END
Source code in operonx/core/ops/base.py
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 | |
Attributes¶
full_name
property
¶
Fully-qualified hierarchical path of this op. Cached after build().
specific_metadata
property
¶
Return subclass-specific metadata. Override in subclasses.
Methods:¶
get
¶
op["key"] for an output this op may not produce.
The build-time check that op["key"] names one of the op's
outputs is skipped, and when the op does not write the key the
reading op gets its own parameter default — the same as for any
input that arrives empty. For an op handed in by someone else,
whose contract makes some outputs optional::
ended = how_it_ended(finish_reason=model.get("finish_reason"))
There is no default= here: the reading op's parameter default
is the one place a default lives.
Source code in operonx/core/ops/base.py
on_error
¶
Error edge: handler runs when this op fails, and only then.
::
@op
def apologise(error: str, op: str, inputs: dict) -> dict:
return {"reply": "Sorry, try again later."}
look = lookup(order=order)
sorry = apologise()
look.on_error(sorry) # sorry runs once if look failed
[look, sorry] >> reply # reply merges the two by itself
The handler runs once per failed invocation of this op — after its
last attempt when it has retry= — and waits for its other
predecessors like any op. Its parameters named error (the error
as "TypeName: message"), op (this op's full name) and
inputs (what this op was called with) receive the failure,
unless they are wired to something else. The failure is still
recorded in "$errors"; Operon(g, errors="raise") does not
end the run for it.
Returns handler, so look.on_error(sorry) >> reply reads on.
Source code in operonx/core/ops/base.py
warmup
¶
Called by Operon engine after graph.build() when a ResourceHub is available.
Override in provider ops (LLMOp, EmbeddingOp, etc.) to eagerly initialize backends and eliminate cold-start latency on the first user request.
The default implementation is a no-op — subclasses opt in by overriding.
Source code in operonx/core/ops/base.py
get_inputs
¶
Retrieve input values from state based on connection mappings.
Each input's cell index is resolved once per schema (see
_build_input_cache); every call, the first included, then reads
each input the same way:
- a shared cell — its one value;
- the op's own cell at exactly this context;
- with a pull ref, the source cell (nearest enclosing context with a value), cached into the op's cell;
- without one, the op's own cell at the nearest enclosing context — a value pushed at a parent context;
then the literal or declared default in place of None,
SCRATCH[...] resolved, and a top-level Media unwrapped so
the op body receives the raw value.
The first call used to go through state[...] instead, and the
two disagreed: only the first unwrapped Media, and only later
calls did step 4's walk. The cache is keyed by the schema, which
every run of an engine and every item of a fan-out share — so an
op's body got a different type, or None instead of a value,
depending on whether it had run before.
Source code in operonx/core/ops/base.py
get_outputs
¶
Read output values from state.
Reads directly from this op's output variables. Output connections (outputs={...}) are resolved by the schema at build time — they create refs at the destination, not at this op.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
MemoryState
|
Workflow state. |
required |
context_id
|
str
|
Context of this op. |
required |
Source code in operonx/core/ops/base.py
normalize_trace_io
¶
Produce a trace-time view of this op's I/O.
Called on the trace's own copies — already filtered by
@op(exclude=/include=) — each time an OpExecution is built
(:meth:_trace_inputs, :meth:_trace_outputs). Subclasses
override to add what the trace should show and the raw I/O does
not: LLMOp adds the rendered request (messages) and wraps
OpenAI chat-format image_url blocks as Media. The real state
value is untouched — return new dicts, never edit these in place.
Built from the filtered copy, so a value hidden from the trace cannot come back through something derived from it.
Default is identity: most ops never override.
Source code in operonx/core/ops/base.py
store_result
¶
Store result dict into state.
Uses state[op, var, ctx] = value for O(1) index-based storage.
Extracts $tags special key for dynamic tagging. After all writes
commit, calls state.advance_step() so the checkpointer /
tracer see a fresh step_id for the next op's writes.
A bare return value has already been wrapped into {output: value}
by _as_result_dict, so return False arrives here as a non-empty
dict and survives the falsy early-out below. That ordering matters:
skipping the write would leave the cell unset, which every reader
downstream sees as "the op did not run" rather than "it said no".
Source code in operonx/core/ops/base.py
save_all_caches
staticmethod
¶
Write every file-backed op cache to its file. Returns entries written.
run
async
¶
run(
state: MemoryState, context_id: Optional[str] = None
) -> AsyncGenerator[tuple[Optional[str], Dict[str, Any]], None]
Execute this op as a uniform async generator.
Every op — whether it uses return or yield — is driven through
the same three-layer model:
- User function (
@op): plainreturn {"k": v}oryield {"k": v}. No awareness of Frame/EOF. BaseOp.run()(this method): wraps the user function via_exec_core()into a uniform async generator that yields(context_id, result)tuples. Normal op → one yield. Generator op → N yields, each in its own stream context[i].Scheduler._pump(): consumes this generator. Each yield becomes aFrameevent on the queue; when the generator exhausts naturally,_pumpemits oneEOFevent. The user never writes Frame or EOF.
Yields:
| Type | Description |
|---|---|
AsyncGenerator[tuple[Optional[str], Dict[str, Any]], None]
|
|
Source code in operonx/core/ops/base.py
1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 | |
serialize
¶
This op as a config dict (see GraphOp.serialize).
Source code in operonx/core/ops/base.py
DummyOp
¶
Bases: BaseOp
Sentinel op used as START, END, and PARENT markers.
Source code in operonx/core/ops/_edges.py
Methods:¶
declare
¶
declare(
*,
reducers: dict | None = None,
allow_race: bool | list | tuple | set = False,
**vars: Any,
) -> None
Declare shared vars on the current graph, with optional reducers.
Shared vars persist across all stream contexts within the graph. Normal
PARENT vars are copied per stream context. When a reducer is registered
for a var, concurrent/repeated writes to its shared cell go through
reducer(old, new) instead of overwriting. Without a reducer, shared
cell writes use last-write-wins semantics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reducers
|
dict | None
|
Optional |
None
|
allow_race
|
bool | list | tuple | set
|
Two ops that may run at once writing a declared cell
without a reducer fail the build: the cell keeps whichever
write lands last. |
False
|
**vars
|
Any
|
|
{}
|
Raises:
| Type | Description |
|---|---|
TypeError
|
if called on anything other than PARENT. |
RuntimeError
|
if called outside a |
ValueError
|
if a reducer key is not in the declared vars. |
Usage::
from operonx.reducers import add_messages
@graph
def agent():
PARENT.declare(
count=0,
messages=[],
reducers={"messages": add_messages},
)
Source code in operonx/core/ops/_edges.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | |
ScratchAccessor
¶
Dict-like accessor for per-call scratch space.
- Inside an op body (ContextVar bound): reads/writes the live scratch dict on the current MemoryState.
- At graph-construction time (ContextVar unbound):
__getitem__returns aScratchRefmarker, post-resolved byBaseOp.get_inputs().__setitem__raises — write-outside-run is a programming error.
Methods:¶
get
¶
Read key, or default when it is absent.
SCRATCH[key] already returns None for a missing key, so
this adds nothing but the idiom people reach for first. Without
it, SCRATCH.get("k") raised AttributeError from inside an
op body — where BaseOp.run records it into state rather than
raising, so it surfaced as an op error rather than an obvious typo.
Outside a run this returns default rather than a
:class:ScratchRef: a ref is a wiring marker and get() with
a default reads as a value lookup, so returning one would smuggle
a marker into a place expecting data.
Source code in operonx/core/ops/_edges.py
keys
¶
items
¶
(key, value) pairs — a snapshot, empty outside a run.
SoftEdge
¶
Branch
¶
Fluent builder for creating a BranchOp.
Two usage flavors — both supported, both idiomatic:
Inline form (recommended for common if/else) — pass op instances as
targets. The Branch auto-wires branch >> target edges at build time
so you never write them yourself, and the branch can drop right into a
>> chain::
START >> source >> if_(source["kind"] == "audio", asr).else_(skip_stt)
asr >> denoise >> picker
skip_stt >> picker
Auto-name resolves to the LHS if there is one (stt_route = if_(...));
inline, to a per-graph counter, route_1, route_2.
Named form (for forward refs or a shared branch node) — pass op
names as strings. No auto-wiring; you wire branch >> target
yourself as before::
router = (if_(PARENT["score"] >= 90, "excellent")
.if_(PARENT["score"] >= 70, "good")
.else_("fail"))
# ...
router >> excellent >> merge
router >> good >> merge
router >> fail >> merge
Mixed is allowed — string targets skip auto-wiring, op-instance targets get auto-wired.
Initialise the builder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
Optional[str]
|
Op name. If None, auto-inferred from the variable name. |
None
|
Source code in operonx/core/ops/flow/branch_op.py
Methods:¶
if_
¶
Add a condition–target case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
condition
|
Union[Ref, BaseOp]
|
Ref with comparison (e.g., |
required |
target
|
Union[str, BaseOp]
|
Target op instance (enables auto-wiring) or op name string. |
required |
Returns:
| Type | Description |
|---|---|
Branch
|
self for chaining. |
Source code in operonx/core/ops/flow/branch_op.py
else_
¶
BranchOp
¶
BranchOp(
cases: Optional[List[Tuple[Ref, str]]] = None,
candidates: Optional[List[str]] = None,
default: Optional[str] = None,
inputs: Dict[str, Any] = None,
outputs: Dict[str, Any] = None,
max_iterations: Optional[int] = None,
**kwargs,
)
Bases: BaseOp
Op that evaluates conditions and routes execution to different targets.
Conditions are Ref objects with comparison operators. The first matching
condition determines the target. An optional anchor input overrides all
conditions. Use soft edges (>>~) to connect branch targets to a merge op.
Inputs
anchor (str, optional): Hard-coded target name that overrides conditions. (any): Variables referenced in condition Refs (auto-extracted).
Outputs
target (str): Name of the selected target op. matched (str): Description of which condition matched.
Example::
router = if_(PARENT["score"] >= 90, "excellent").else_("fail")
START >> router >> ~excellent >> merge >> END
router >> ~fail >> merge
Source code in operonx/core/ops/flow/branch_op.py
EmitOp
¶
Bases: BaseOp
Op that emits a CustomEvent to mode="custom" subscribers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
Any
|
Value (typically a |
None
|
channel
|
str
|
Free-form string label used by consumers for filtering.
Defaults to |
'default'
|
Behaviour
- Fires once per invocation (per ctx for streaming/parallel graphs).
- Fire-and-forget: no subscriber → payload dropped, no error.
- Emits
CustomEvent(step_id, op, ctx, channel, payload)viastate._notify_custom— scheduler providesstep_idandctxvia ContextVar plumbing (matchesSCRATCH's pattern).
Source code in operonx/core/ops/flow/emit_op.py
InterruptOp
¶
Bases: BaseOp
Op that suspends until the caller posts a resume value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
Any
|
Value (typically a |
None
|
timeout
|
float
|
Optional wall-clock seconds. Falsey (0/None) = wait forever. A run with a journal does not wait: it parks (below), and the timeout does not apply. |
0
|
Behaviour
- Emits
InterruptEvent(step_id, op, ctx, payload, interrupt_id)when dispatched.interrupt_idis :func:~operonx.core.runtime.invocation_keyof (run, op, ctx): deterministic, the same as the op'srun_context().idempotency_key. - Awaits
state._interrupt_responses[interrupt_id](adict[str, Future]) — the caller resolves this future. - Returns
{"response": <value>}— downstream refs likeapprove["response"]read the answer. - Also exposes
timed_out(bool) andinterrupt_id(str) outputs. - With
Operon(journal=…)the run parks instead: the question is journalled, the ops in flight finish, nothing new starts, and the run stops with statusinterrupted— its result holds"$interrupted": [{interrupt_id, op, ctx, payload}].await engine.resume(run_id, answers={interrupt_id: value}), in any process, continues it withvalueas the response.
Design note
This op piggybacks on the state's interrupt bus.
engine.stream(mode="updates" | "interrupts") subscribes to it
and yields each event; the event's resume(value) resolves the
future this op awaits.
Source code in operonx/core/ops/flow/interrupt_op.py
GraphOp
¶
Bases: BaseOp
Container op that holds and executes a directed graph of child ops.
Lifecycle::
1. DEFINE with GraphOp(name="wf") as g:
a = double(x=PARENT["x"])
b = add(a=a["result"], b=PARENT["y"])
START >> a >> b >> END
Ops auto-register via context manager. Edges via >> operator.
Inputs/outputs auto-discovered from PARENT refs.
2. BUILD g.build() (or auto on first run)
_setup_schema scan PARENT refs → graph inputs/outputs
_setup_endpoints find entry/exit ops from topology
_build() adj list + ready counts + stream ready counts
validate branch targets, cycles, reachability, refs
3. EXECUTE g.run(state, context_id) — async generator
→ run_task_scheduler() drives ops via Frame/EOF events
→ yields (ctx, outputs) per batch or per stream frame
→ loop iteration handled inside scheduler EOF handler
4. EXPORT serialize() the graph as a config dict
validate() graph structure validation
show() debug display
Source code in operonx/core/ops/graph/graph_op.py
Methods:¶
get_current_graph
staticmethod
¶
add_op
¶
Add an op to the graph.
Source code in operonx/core/ops/graph/graph_op.py
add_edge
¶
add_edge(
source: str,
target: str,
type: EdgeType = "normal",
soft: bool = False,
hard: bool = False,
)
Add an edge between two ops.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str
|
Source op name. |
required |
target
|
str
|
Target op name. |
required |
type
|
EdgeType
|
Edge type (normal, lookback, condition). |
'normal'
|
soft
|
bool
|
If True, edge does not count toward ready_count. Used for branch outputs when only one branch executes. |
False
|
hard
|
bool
|
If True, opt this edge out of auto-softening at build time. Use for the rare case where a branch-descended pred must still be waited on as a hard dependency. |
False
|
Source code in operonx/core/ops/graph/graph_op.py
build
¶
Build graph: cycle-rewrite → children → schema → endpoints → validation → topology.
The cycle-rewrite pass (Phase 3 Level-2) runs FIRST so that any
synthetic hidden GraphOp.loop children it creates are then built
alongside the user's own children. Ordering matters — validate() and
auto_soft assume a DAG and would produce wrong results on cyclic input.
Source code in operonx/core/ops/graph/graph_op.py
store_result
¶
Store the graph's outputs — except its declared cells.
A declared cell already holds the graph's value: every write inside
the graph went into it. Storing the graph's output there again hands
the cell's reducer its own accumulated value as a fresh delta — an
add_messages cell came out with every message twice.
Source code in operonx/core/ops/graph/graph_op.py
run
async
¶
run(
state: MemoryState, context_id: Optional[tuple] = None
) -> AsyncGenerator[Tuple[tuple, Dict[str, Any]], None]
Execute graph: get inputs → schedule ops → loop if needed → store results.
Source code in operonx/core/ops/graph/graph_op.py
931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 | |
serialize
¶
The whole graph as a config dict: ops, edges, entries and exits.
A graph's canonical description; an eval's fingerprint hashes it.
It was written for the dropped Rust runtime, and some keys are
still that runtime's (initial_ready_count, compiled_adj,
stream_initial_ready): kept so the output, and every hash
taken of it, stays the same.
Source code in operonx/core/ops/graph/graph_op.py
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 | |
validate
¶
Run all validations and return result.
Source code in operonx/core/ops/graph/graph_op.py
show
¶
Display graph structure (debug).
Source code in operonx/core/ops/graph/graph_op.py
FuncOp
¶
FuncOp(
code_fn: Optional[Callable] = None,
return_keys: Optional[List[str]] = None,
inputs: Dict[str, Any] = None,
outputs: Dict[str, Any] = None,
_mappings: Dict[str, Any] = None,
**kwargs,
)
Bases: BaseOp
Op that executes a Python function.
Inputs and outputs are auto-extracted from the function's signature and
return-statement AST. Both sync and async functions are supported.
Prefer the @op decorator over instantiating FuncOp directly.
Inputs
Auto-parsed from the function's parameter list.
Outputs
Auto-parsed from return {"key": ...} via AST, or from
explicit return_keys.
Example::
@op
def add(a: int, b: int):
return {"sum": a + b}
with GraphOp(name="main") as graph:
result = add(a=PARENT["x"], b=PARENT["y"])
START >> result >> END
Source code in operonx/core/ops/transform/func_op.py
Functions:¶
shorthand
¶
Decorator for Op.of() classmethods.
Registers the function for auto-naming frame skip via register_skip()
and wraps as classmethod.
Usage::
class MyOp(BaseOp):
@shorthand
def of(cls, my_param=None, **kwargs):
inputs, init_kwargs = split_shorthand_kwargs(kwargs)
return cls(my_param=my_param, inputs=inputs or None, **init_kwargs)
Source code in operonx/core/ops/_shortcuts.py
split_shorthand_kwargs
¶
split_shorthand_kwargs(
kwargs: dict, extra_init_keys: set = None, own: frozenset = frozenset()
) -> tuple
Split flat kwargs into (inputs, init_kwargs).
Used by shorthand functions (llm_, for_, op, etc.) to separate op constructor kwargs from input mappings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kwargs
|
dict
|
Flat keyword arguments from shorthand function. |
required |
extra_init_keys
|
set
|
Additional op-specific init keys beyond base keys (e.g., {'max_concurrency', 'callback'} for iteration ops). |
None
|
own
|
frozenset
|
The function's own parameter names. A keyword among them is
always an input — |
frozenset()
|
Returns:
| Type | Description |
|---|---|
tuple
|
(inputs, init_kwargs) tuple where: |
tuple
|
|
tuple
|
|
Example
Provider ops - just base keys¶
inputs, init_kwargs = split_shorthand_kwargs(kwargs)
Iteration ops - with extra keys¶
inputs, init_kwargs = split_shorthand_kwargs( kwargs, {'max_concurrency', 'until', 'callback'} )
Source code in operonx/core/ops/_shortcuts.py
if_
¶
if_(
condition: Union[Ref, BaseOp],
target: Union[str, BaseOp],
*,
max_iterations: Optional[int] = None,
) -> Branch
Start a branch declaration with the first condition.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
condition
|
Union[Ref, BaseOp]
|
What to test — a Ref comparison or a single-output op. |
required |
target
|
Union[str, BaseOp]
|
The op (or op name) to route to when it holds. |
required |
max_iterations
|
Optional[int]
|
When one of this branch's arms loops back to an
earlier op, the most iterations that loop may run (default
1000). A loop that reaches it stops, records
|
None
|
Example (inline, auto-wired)::
START >> source >> if_(cond, asr).else_(skip_stt)
Example (named, string targets, wire manually)::
router = if_(PARENT["score"] >= 90, "excellent").else_("fail")
router >> excellent >> merge
router >> fail >> merge
Example (a loop capped at 50 iterations)::
START >> s >> if_(s["done"] == True, END, max_iterations=50).else_(s)
Source code in operonx/core/ops/flow/branch_op.py
op
¶
op(
func: Optional[Callable] = None,
*,
bound: Optional[str] = None,
cache: Optional[Any] = None,
delay: float = 0,
exclude: Optional[Any] = None,
include: Optional[Any] = None,
observe_max: Optional[int] = None,
transient: bool = False,
show_keys: Optional[Any] = None,
door: Optional[str] = None,
retry: Optional[Retry] = None,
timeout: Optional[Timeout] = None,
) -> Any
Decorator that turns a plain function into a FuncOp factory.
Can be used bare or with keyword arguments::
@op
def double(x: int):
return {"result": x * 2}
@op(bound="cpu")
def heavy_compute(data: list):
return {"result": process(data)}
@op(bound="io")
async def call_api(url: str):
return {"data": await fetch(url)}
# Phase 2 observability filter (Checkpointer + Tracer both respect these):
@op(exclude=["tokens"]) # skip these vars in both observers
@op(include=[]) # silence entire op (allowlist of nothing)
@op(exclude={"trace": ["pii"]}) # per-observer split (dict form)
@op(observe_max=10_000) # circuit breaker: raise if op emits > N events
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bound
|
Optional[str]
|
Execution bound hint for the scheduler.
|
None
|
exclude
|
Optional[Any]
|
Vars to hide from observers. |
None
|
include
|
Optional[Any]
|
Vars to expose to observers (allowlist). Same shapes as |
None
|
transient
|
bool
|
This op's per-item outputs are stored, delivered, then evicted when the consuming context finishes, instead of being kept for the life of the run. For high-rate streams — a socket reader, an audio chunker — where retaining every item is the difference between a flat run and one that grows without bound. Transience propagates along pull refs: a cell that caches a pull
from a transient port is itself transient, so declaring it once on
the producer stops the whole chain from retaining. Incompatible
with |
False
|
observe_max
|
Optional[int]
|
Per-op circuit breaker. If the op emits more than this many
events in a single run, :class: |
None
|
show_keys
|
Optional[Any]
|
The output key (or keys) that stand for this op — what a viewer
prints for it when it has room for one line instead of a port
list. |
None
|
door
|
Optional[str]
|
|
None
|
retry
|
Optional[Retry]
|
|
None
|
timeout
|
Optional[Timeout]
|
|
None
|
Source code in operonx/core/ops/transform/func_op.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |