matrx-graph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrx_graph/__init__.py +236 -0
- matrx_graph/actions/ACTION_STANDARDS.md +165 -0
- matrx_graph/actions/__init__.py +36 -0
- matrx_graph/actions/decorator.py +273 -0
- matrx_graph/actions/registry.py +67 -0
- matrx_graph/actions/spec.py +160 -0
- matrx_graph/checkpoint/__init__.py +37 -0
- matrx_graph/checkpoint/memory.py +60 -0
- matrx_graph/checkpoint/postgres.py +284 -0
- matrx_graph/checkpoint/protocol.py +247 -0
- matrx_graph/compile.py +412 -0
- matrx_graph/contracts/__init__.py +39 -0
- matrx_graph/contracts/audit.py +595 -0
- matrx_graph/db/__init__.py +76 -0
- matrx_graph/db/_registry.py +105 -0
- matrx_graph/db/db_requirements.py +57 -0
- matrx_graph/db/definition_store.py +420 -0
- matrx_graph/db/event_store.py +221 -0
- matrx_graph/db/migrations/0020_wf_run_announce.sql +66 -0
- matrx_graph/db/migrations/0021_join_semantics.sql +30 -0
- matrx_graph/db/migrations/0022_node_data_slot.sql +109 -0
- matrx_graph/db/migrations/0023_fanout_identity.sql +55 -0
- matrx_graph/db/migrations/0024_definition_variables.sql +17 -0
- matrx_graph/db/migrations/0100_baseline_post_reorg.sql +878 -0
- matrx_graph/db/migrations/README.md +89 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0001_wf_initial.sql +271 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0002_wf_queue.sql +68 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0003_wf_node_events.sql +82 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0004_wf_trigger.sql +88 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0005_wf_rls.sql +211 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0006_wf_run_cancelling.sql +25 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0007_wf_run_paused_and_errored.sql +36 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0008_wf_node_outcome_source.sql +30 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0009_wf_run_recovery_limit.sql +42 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0010_wf_idempotency.sql +73 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0011_wf_definition_concurrency_limit.sql +37 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0012_wf_trigger_fire.sql +52 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0013_wf_recovery_audit.sql +46 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0014_wf_node_events_notify.sql +43 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0016_wf_template.sql +62 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0017_wf_hardening.sql +27 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0018_wf_node_events_seq.sql +62 -0
- matrx_graph/db/migrations/legacy_pre_reorg/0019_wf_node_events_drop_check.sql +21 -0
- matrx_graph/db/migrations/legacy_pre_reorg/README.md +28 -0
- matrx_graph/db/node_data_slot_store.py +312 -0
- matrx_graph/db/pool.py +56 -0
- matrx_graph/db/run_store.py +625 -0
- matrx_graph/db/trigger_store.py +291 -0
- matrx_graph/db/wf_manager.py +53 -0
- matrx_graph/diagnostics/__init__.py +0 -0
- matrx_graph/diagnostics/compile_validation.py +172 -0
- matrx_graph/dry_run.py +547 -0
- matrx_graph/errors.py +160 -0
- matrx_graph/examples/__init__.py +7 -0
- matrx_graph/examples/self_validating_news.py +335 -0
- matrx_graph/executor/__init__.py +23 -0
- matrx_graph/executor/_retry.py +50 -0
- matrx_graph/executor/channels.py +151 -0
- matrx_graph/executor/introspect.py +57 -0
- matrx_graph/executor/json_dump.py +49 -0
- matrx_graph/executor/primitives.py +11 -0
- matrx_graph/executor/registry.py +142 -0
- matrx_graph/executor/scheduler.py +3507 -0
- matrx_graph/executor/schema_validation.py +67 -0
- matrx_graph/kinds.py +187 -0
- matrx_graph/nodes/__init__.py +159 -0
- matrx_graph/nodes/_sandbox.py +93 -0
- matrx_graph/nodes/control/__init__.py +50 -0
- matrx_graph/nodes/control/branch.py +110 -0
- matrx_graph/nodes/control/gather.py +194 -0
- matrx_graph/nodes/control/human_input.py +108 -0
- matrx_graph/nodes/control/loop.py +148 -0
- matrx_graph/nodes/control/map.py +162 -0
- matrx_graph/nodes/control/send.py +109 -0
- matrx_graph/nodes/crypto/__init__.py +25 -0
- matrx_graph/nodes/crypto/ops.py +235 -0
- matrx_graph/nodes/data/__init__.py +1 -0
- matrx_graph/nodes/data/assert_.py +96 -0
- matrx_graph/nodes/data/filter.py +92 -0
- matrx_graph/nodes/data/json_io.py +135 -0
- matrx_graph/nodes/data/merge.py +67 -0
- matrx_graph/nodes/data/pick_omit.py +126 -0
- matrx_graph/nodes/data/transform.py +91 -0
- matrx_graph/nodes/datetime_/__init__.py +21 -0
- matrx_graph/nodes/datetime_/ops.py +311 -0
- matrx_graph/nodes/http/__init__.py +23 -0
- matrx_graph/nodes/http/_shared.py +119 -0
- matrx_graph/nodes/http/custom_api.py +294 -0
- matrx_graph/nodes/http/get.py +103 -0
- matrx_graph/nodes/http/graphql.py +167 -0
- matrx_graph/nodes/http/post.py +128 -0
- matrx_graph/nodes/io/__init__.py +10 -0
- matrx_graph/nodes/io/user_input.py +235 -0
- matrx_graph/nodes/output/__init__.py +9 -0
- matrx_graph/nodes/output/to_frontend.py +171 -0
- matrx_graph/nodes/subgraph/__init__.py +5 -0
- matrx_graph/nodes/subgraph/call.py +162 -0
- matrx_graph/nodes/text/__init__.py +37 -0
- matrx_graph/nodes/text/custom_extract.py +387 -0
- matrx_graph/nodes/text/json_path.py +156 -0
- matrx_graph/nodes/text/manipulate.py +401 -0
- matrx_graph/nodes/text/regex.py +271 -0
- matrx_graph/nodes/tool/__init__.py +5 -0
- matrx_graph/nodes/tool/call.py +163 -0
- matrx_graph/observability/__init__.py +117 -0
- matrx_graph/py.typed +0 -0
- matrx_graph/references.py +113 -0
- matrx_graph/types/__init__.py +107 -0
- matrx_graph/types/canonical_payloads.py +138 -0
- matrx_graph/types/channel.py +73 -0
- matrx_graph/types/compiled.py +149 -0
- matrx_graph/types/context.py +225 -0
- matrx_graph/types/definition.py +240 -0
- matrx_graph/types/events.py +424 -0
- matrx_graph/types/handle.py +39 -0
- matrx_graph/types/node_spec.py +209 -0
- matrx_graph/types/primitives.py +117 -0
- matrx_graph/types/result.py +133 -0
- matrx_graph/types/run.py +39 -0
- matrx_graph/types/send.py +59 -0
- matrx_graph/types/usl.py +112 -0
- matrx_graph/types/value_objects.py +36 -0
- matrx_graph/types/variables.py +143 -0
- matrx_graph/validation.py +559 -0
- matrx_graph/workers/__init__.py +62 -0
- matrx_graph/workers/cron_watcher.py +100 -0
- matrx_graph/workers/queue.py +360 -0
- matrx_graph/workers/runner.py +164 -0
- matrx_graph-0.1.0.dist-info/METADATA +110 -0
- matrx_graph-0.1.0.dist-info/RECORD +131 -0
- matrx_graph-0.1.0.dist-info/WHEEL +4 -0
matrx_graph/__init__.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""matrx-graph — channel-based, super-step workflow orchestration engine."""
|
|
2
|
+
|
|
3
|
+
from matrx_graph.actions import (
|
|
4
|
+
ActionRegistry,
|
|
5
|
+
ActionSpec,
|
|
6
|
+
action,
|
|
7
|
+
default_action_registry,
|
|
8
|
+
iter_registered_action_specs,
|
|
9
|
+
resolve_action_executor,
|
|
10
|
+
)
|
|
11
|
+
from matrx_graph.checkpoint import (
|
|
12
|
+
Checkpoint,
|
|
13
|
+
Checkpointer,
|
|
14
|
+
MemoryCheckpointer,
|
|
15
|
+
NextInvocation,
|
|
16
|
+
NodeOutcome,
|
|
17
|
+
PendingBinding,
|
|
18
|
+
PendingEdgeBinding,
|
|
19
|
+
)
|
|
20
|
+
from matrx_graph.compile import compile as compile_graph
|
|
21
|
+
from matrx_graph.dry_run import (
|
|
22
|
+
DryRunEdgeReport,
|
|
23
|
+
DryRunNodeReport,
|
|
24
|
+
DryRunReport,
|
|
25
|
+
DryRunSample,
|
|
26
|
+
MappingIssue,
|
|
27
|
+
MissingBinding,
|
|
28
|
+
dry_run,
|
|
29
|
+
)
|
|
30
|
+
from matrx_graph.errors import (
|
|
31
|
+
ActionAuthorizationError,
|
|
32
|
+
ChannelError,
|
|
33
|
+
CheckpointError,
|
|
34
|
+
CompilationError,
|
|
35
|
+
DefinitionError,
|
|
36
|
+
EdgeRoutingError,
|
|
37
|
+
ExecutionError,
|
|
38
|
+
GraphInterrupt,
|
|
39
|
+
MatrxGraphError,
|
|
40
|
+
NodeFailureError,
|
|
41
|
+
NodeInputValidationError,
|
|
42
|
+
NodeRegistryError,
|
|
43
|
+
UnresolvedVariableError,
|
|
44
|
+
)
|
|
45
|
+
from matrx_graph.executor import (
|
|
46
|
+
ChannelManager,
|
|
47
|
+
Invocation,
|
|
48
|
+
NodeExecutor,
|
|
49
|
+
NodeRegistry,
|
|
50
|
+
RunResult,
|
|
51
|
+
Scheduler,
|
|
52
|
+
Send,
|
|
53
|
+
default_registry,
|
|
54
|
+
register,
|
|
55
|
+
)
|
|
56
|
+
from matrx_graph.kinds import (
|
|
57
|
+
KindEntry,
|
|
58
|
+
get_kind,
|
|
59
|
+
invalidate_kind_catalog_cache,
|
|
60
|
+
validate_against_kind,
|
|
61
|
+
)
|
|
62
|
+
from matrx_graph.nodes import register_builtin_nodes
|
|
63
|
+
from matrx_graph.references import (
|
|
64
|
+
ReferenceResolver,
|
|
65
|
+
configure_reference_resolver,
|
|
66
|
+
is_reference_envelope,
|
|
67
|
+
is_reference_resolver_configured,
|
|
68
|
+
resolve_references,
|
|
69
|
+
)
|
|
70
|
+
from matrx_graph.types import (
|
|
71
|
+
ActionTier,
|
|
72
|
+
BackoffStrategy,
|
|
73
|
+
ChannelSpec,
|
|
74
|
+
ChannelView,
|
|
75
|
+
CompiledEdge,
|
|
76
|
+
CompiledGraph,
|
|
77
|
+
CompiledNode,
|
|
78
|
+
Definition,
|
|
79
|
+
EdgeDef,
|
|
80
|
+
EdgeKind,
|
|
81
|
+
RESERVED_PAYLOAD_FIELDS,
|
|
82
|
+
ARCHETYPES,
|
|
83
|
+
BasePayload,
|
|
84
|
+
BulkItem,
|
|
85
|
+
BulkResult,
|
|
86
|
+
EmptyConfig,
|
|
87
|
+
EmptyInput,
|
|
88
|
+
EmptyOutput,
|
|
89
|
+
Failure,
|
|
90
|
+
HandleDirection,
|
|
91
|
+
HandleSpec,
|
|
92
|
+
InterpretedResult,
|
|
93
|
+
Items,
|
|
94
|
+
NodeCategory,
|
|
95
|
+
NodeDef,
|
|
96
|
+
NodeError,
|
|
97
|
+
NodeExecutionContext,
|
|
98
|
+
NodeResult,
|
|
99
|
+
NodeSpec,
|
|
100
|
+
OperationResult,
|
|
101
|
+
Page,
|
|
102
|
+
Position,
|
|
103
|
+
Success,
|
|
104
|
+
Timing,
|
|
105
|
+
Value,
|
|
106
|
+
Reducer,
|
|
107
|
+
RetryPolicy,
|
|
108
|
+
Run,
|
|
109
|
+
RunStatus,
|
|
110
|
+
USLFieldHints,
|
|
111
|
+
VariableSpec,
|
|
112
|
+
Viewport,
|
|
113
|
+
coerce_to_json,
|
|
114
|
+
field_extras,
|
|
115
|
+
model_usl_descriptor,
|
|
116
|
+
failure,
|
|
117
|
+
interpret_node_result,
|
|
118
|
+
success,
|
|
119
|
+
var_ref,
|
|
120
|
+
)
|
|
121
|
+
from matrx_graph.validation import (
|
|
122
|
+
ValidationIssue,
|
|
123
|
+
ValidationResult,
|
|
124
|
+
validate_definition,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
__version__ = "0.1.0"
|
|
128
|
+
|
|
129
|
+
__all__ = [
|
|
130
|
+
"ARCHETYPES",
|
|
131
|
+
"ActionAuthorizationError",
|
|
132
|
+
"ActionRegistry",
|
|
133
|
+
"ActionSpec",
|
|
134
|
+
"ActionTier",
|
|
135
|
+
"BackoffStrategy",
|
|
136
|
+
"BasePayload",
|
|
137
|
+
"BulkItem",
|
|
138
|
+
"BulkResult",
|
|
139
|
+
"ChannelError",
|
|
140
|
+
"ChannelManager",
|
|
141
|
+
"ChannelSpec",
|
|
142
|
+
"ChannelView",
|
|
143
|
+
"Checkpoint",
|
|
144
|
+
"CheckpointError",
|
|
145
|
+
"Checkpointer",
|
|
146
|
+
"CompilationError",
|
|
147
|
+
"CompiledEdge",
|
|
148
|
+
"CompiledGraph",
|
|
149
|
+
"CompiledNode",
|
|
150
|
+
"Definition",
|
|
151
|
+
"DefinitionError",
|
|
152
|
+
"DryRunEdgeReport",
|
|
153
|
+
"DryRunNodeReport",
|
|
154
|
+
"DryRunReport",
|
|
155
|
+
"DryRunSample",
|
|
156
|
+
"EdgeDef",
|
|
157
|
+
"EdgeKind",
|
|
158
|
+
"EdgeRoutingError",
|
|
159
|
+
"EmptyConfig",
|
|
160
|
+
"EmptyInput",
|
|
161
|
+
"EmptyOutput",
|
|
162
|
+
"ExecutionError",
|
|
163
|
+
"Failure",
|
|
164
|
+
"GraphInterrupt",
|
|
165
|
+
"HandleDirection",
|
|
166
|
+
"HandleSpec",
|
|
167
|
+
"InterpretedResult",
|
|
168
|
+
"Invocation",
|
|
169
|
+
"Items",
|
|
170
|
+
"KindEntry",
|
|
171
|
+
"MappingIssue",
|
|
172
|
+
"MatrxGraphError",
|
|
173
|
+
"MemoryCheckpointer",
|
|
174
|
+
"MissingBinding",
|
|
175
|
+
"NextInvocation",
|
|
176
|
+
"NodeCategory",
|
|
177
|
+
"NodeDef",
|
|
178
|
+
"NodeError",
|
|
179
|
+
"NodeExecutionContext",
|
|
180
|
+
"NodeExecutor",
|
|
181
|
+
"NodeFailureError",
|
|
182
|
+
"NodeInputValidationError",
|
|
183
|
+
"NodeOutcome",
|
|
184
|
+
"NodeRegistry",
|
|
185
|
+
"NodeRegistryError",
|
|
186
|
+
"NodeResult",
|
|
187
|
+
"NodeSpec",
|
|
188
|
+
"OperationResult",
|
|
189
|
+
"Page",
|
|
190
|
+
"PendingBinding",
|
|
191
|
+
"PendingEdgeBinding",
|
|
192
|
+
"Position",
|
|
193
|
+
"RESERVED_PAYLOAD_FIELDS",
|
|
194
|
+
"Reducer",
|
|
195
|
+
"ReferenceResolver",
|
|
196
|
+
"RetryPolicy",
|
|
197
|
+
"Run",
|
|
198
|
+
"RunResult",
|
|
199
|
+
"RunStatus",
|
|
200
|
+
"Scheduler",
|
|
201
|
+
"Send",
|
|
202
|
+
"Success",
|
|
203
|
+
"Timing",
|
|
204
|
+
"USLFieldHints",
|
|
205
|
+
"UnresolvedVariableError",
|
|
206
|
+
"Value",
|
|
207
|
+
"ValidationIssue",
|
|
208
|
+
"ValidationResult",
|
|
209
|
+
"VariableSpec",
|
|
210
|
+
"Viewport",
|
|
211
|
+
"__version__",
|
|
212
|
+
"action",
|
|
213
|
+
"coerce_to_json",
|
|
214
|
+
"compile_graph",
|
|
215
|
+
"configure_reference_resolver",
|
|
216
|
+
"default_action_registry",
|
|
217
|
+
"default_registry",
|
|
218
|
+
"dry_run",
|
|
219
|
+
"failure",
|
|
220
|
+
"field_extras",
|
|
221
|
+
"get_kind",
|
|
222
|
+
"invalidate_kind_catalog_cache",
|
|
223
|
+
"is_reference_envelope",
|
|
224
|
+
"interpret_node_result",
|
|
225
|
+
"is_reference_resolver_configured",
|
|
226
|
+
"iter_registered_action_specs",
|
|
227
|
+
"model_usl_descriptor",
|
|
228
|
+
"register",
|
|
229
|
+
"register_builtin_nodes",
|
|
230
|
+
"resolve_action_executor",
|
|
231
|
+
"resolve_references",
|
|
232
|
+
"success",
|
|
233
|
+
"validate_against_kind",
|
|
234
|
+
"validate_definition",
|
|
235
|
+
"var_ref",
|
|
236
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Action / Node Standards — the contract every `@action` and `NodeExecutor` MUST follow
|
|
2
|
+
|
|
3
|
+
> Written after an audit found `aidream/graph_actions/web/{brave,google,news}.py` had drifted
|
|
4
|
+
> from the standard the built-in `http.*` nodes (and the in-package `ai.search.brave` action)
|
|
5
|
+
> already establish. Concretely: `brave.py` was silently turning a **search failure**
|
|
6
|
+
> (`async_brave_search` returning `None` on rate-limit/exhaustion/error — see its own comment,
|
|
7
|
+
> "caller MUST treat as a failure, not an empty result set") into a **successful empty result**
|
|
8
|
+
> (`results: []`). `google.py`/`news.py` let raw provider exceptions kill the whole graph run
|
|
9
|
+
> instead of reporting a structured failure. None of the three had a timeout, an `error` field,
|
|
10
|
+
> or `elapsed_ms`. These rules exist so that class of bug cannot ship again, in either direction.
|
|
11
|
+
|
|
12
|
+
These rules are not style preference — they are the reason `control.branch` /
|
|
13
|
+
`data.transform` can gate on a single field without parsing bodies, the reason a flaky
|
|
14
|
+
third-party API can't take down an entire workflow run, and the reason "it returned nothing"
|
|
15
|
+
and "it broke" stay distinguishable events downstream. **Every new action/node PR is reviewed
|
|
16
|
+
against this list. Every existing one should be brought into compliance opportunistically.**
|
|
17
|
+
|
|
18
|
+
> **2026-07-08 — the Node Result System supersedes the old per-payload
|
|
19
|
+
> `ok`/`success` booleans.** The contract of record is
|
|
20
|
+
> [docs/workflow/NODE_RESULT_CONTRACT.md](../../../../docs/workflow/NODE_RESULT_CONTRACT.md).
|
|
21
|
+
> Every node returns `NodeResult[T] = Success[T] | Failure`
|
|
22
|
+
> (`matrx_graph.types.result`, via the `success()`/`failure()` factories);
|
|
23
|
+
> the scheduler unwraps it. Rules below marked **(superseded)** describe the
|
|
24
|
+
> pre-envelope convention still present on unmigrated nodes — do NOT copy
|
|
25
|
+
> them into new or migrated code.
|
|
26
|
+
|
|
27
|
+
Reference implementations to copy from, in order of relevance:
|
|
28
|
+
- [`nodes/http/_shared.py`](../nodes/http/_shared.py) + [`get.py`](../nodes/http/get.py) /
|
|
29
|
+
[`post.py`](../nodes/http/post.py) / [`graphql.py`](../nodes/http/graphql.py) /
|
|
30
|
+
[`custom_api.py`](../nodes/http/custom_api.py) — canonical MIGRATED raw
|
|
31
|
+
`NodeExecutor`s: `NodeResult[HttpResponse]`, transport failure →
|
|
32
|
+
`failure("http_transport_error", ...)`, retry/timeout guardrails.
|
|
33
|
+
- [`aidream/graph_actions/admin/sql.py`](../../../../aidream/graph_actions/admin/sql.py) —
|
|
34
|
+
canonical MIGRATED `@action` family: stable failure codes, composition via
|
|
35
|
+
pass-through of the inner action's `NodeResult`.
|
|
36
|
+
- [`matrx_ai/graph_nodes/llm_action.py`](../../../matrx-ai/matrx_ai/graph_nodes/llm_action.py) +
|
|
37
|
+
`shared.py::normalize_completed_result` — MIGRATED AI action: failed turn →
|
|
38
|
+
`Failure("ai_turn_failed")` with billed usage under `error.details.usage`.
|
|
39
|
+
- [`aidream/graph_actions/_shared.py`](../../../../aidream/graph_actions/_shared.py) —
|
|
40
|
+
`gather_partial` / `run_with_fallback` / `require_any` for multi-item fan-outs.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 1. I/O contract
|
|
45
|
+
|
|
46
|
+
1. **Pydantic models for every input and every result — success AND failure.** No raw dicts
|
|
47
|
+
crossing the node boundary, ever.
|
|
48
|
+
2. **Input models default to `ConfigDict(extra="forbid")`.** Unknown keys are almost always a
|
|
49
|
+
typo or a stale caller — fail loud, not silently. `extra="allow"` is an exception that must
|
|
50
|
+
be justified in a comment (e.g. `ToolCallInput`/`ToolCallOutput` are deliberately passthrough
|
|
51
|
+
envelopes; result-item models like `BraveSearchResult` allow extra because the upstream API's
|
|
52
|
+
shape is loosely specified) — never the default choice out of laziness.
|
|
53
|
+
3. **Return `NodeResult[T]`: `success(payload)` on success, `failure(code, message,
|
|
54
|
+
details=...)` when the node cannot do its job.** The payload T must NOT carry a
|
|
55
|
+
top-level `ok`, `success`, or `error` field (reserved — CI-guarded); failure semantics
|
|
56
|
+
live ONLY in the envelope. `NodeError.code` is a stable snake_case key sharing the
|
|
57
|
+
`RetryPolicy.retry_on` namespace with exception class names. *(Superseded rule: the
|
|
58
|
+
old per-payload `ok: bool`/`success: bool` + `error: str|None` convention — present
|
|
59
|
+
only on unmigrated nodes.)*
|
|
60
|
+
4. **Never let a failure signal from the wrapped function get silently coerced into a
|
|
61
|
+
"successful empty" result.** If the thing you're calling can return `None`/`{}`/an empty
|
|
62
|
+
collection to mean *"it broke"* as well as to mean *"it genuinely found nothing,"* you MUST
|
|
63
|
+
preserve that distinction — return `failure(...)`, never an empty `success(...)`. When in
|
|
64
|
+
doubt, read the wrapped function's own docstring/comments for exactly this warning
|
|
65
|
+
(several already document it explicitly).
|
|
66
|
+
5. **Every field gets `Field(..., description=...)`.** The schema is user-facing — it renders in
|
|
67
|
+
the Studio palette.
|
|
68
|
+
6. **Known nested shapes get their own Pydantic model**, not `dict[str, Any]`. `extra="allow"`
|
|
69
|
+
on a *result-item* model (e.g. `BraveSearchResult`) is fine when the upstream API's schema is
|
|
70
|
+
loose but you still want the fields you rely on typed.
|
|
71
|
+
7. **The action must return a `BaseModel` instance**, never a dict — `tool.call` raises
|
|
72
|
+
`ExecutionError` if it doesn't, and the output must `model_dump(mode="json")` cleanly (no
|
|
73
|
+
non-JSON-serializable types) since checkpointing and `tool.call`'s field-spreading both
|
|
74
|
+
depend on it.
|
|
75
|
+
|
|
76
|
+
## 2. Error handling — the node's job is to never let the run die of surprise
|
|
77
|
+
|
|
78
|
+
8. **Every call to an external system (HTTP, SDK, subprocess) is wrapped in its own
|
|
79
|
+
`try/except`.** The `@action` executor boundary does **not** catch for you —
|
|
80
|
+
`_ActionExecutor.execute` calls `await self._fn(ctx, inputs)` bare. An uncaught exception
|
|
81
|
+
propagates straight to the scheduler and fails the node/run outright.
|
|
82
|
+
9. **On failure, return `failure(code, message, details=...)` — don't raise** — unless the
|
|
83
|
+
failure is a genuine authoring/programming error (bad config, an invariant your own code
|
|
84
|
+
controls). Both paths land in the SAME scheduler ladder (retry → ERROR-edge route /
|
|
85
|
+
fan-out hole / park), but a returned Failure carries a stable `code` and structured
|
|
86
|
+
`details` — strictly better for consumers. A failed PAID call puts its usage under
|
|
87
|
+
`details["usage"]` so cost settlement survives.
|
|
88
|
+
10. **Time the call and report it: `elapsed_ms: int`** (canonical name — the `Timing` value
|
|
89
|
+
object in `matrx_graph.types.value_objects`). Every built-in `http.*` node does this;
|
|
90
|
+
it's the first thing you want when debugging a slow/failed run.
|
|
91
|
+
|
|
92
|
+
## 3. Timeouts & size guardrails
|
|
93
|
+
|
|
94
|
+
11. **Every network/external call has an explicit, bounded timeout** — never rely on the
|
|
95
|
+
underlying library's default (which may be "wait forever"). Expose it as a field
|
|
96
|
+
(`timeout_s`/`timeout`) with `ge=`/`le=` bounds via Pydantic `Field`, and if the wrapped
|
|
97
|
+
callable doesn't accept a timeout itself, wrap the call in `asyncio.wait_for(...)`.
|
|
98
|
+
12. **Anything that can return an unbounded-size payload caps it** (`max_bytes`/`max_items`/
|
|
99
|
+
similar, sane default + bounds) — same doctrine as `TOOL_RESULT_SIZE_GATE.md` for LLM tools
|
|
100
|
+
applies equally here: the action that can return something big owns its own truncation.
|
|
101
|
+
|
|
102
|
+
## 4. Config vs. business input
|
|
103
|
+
|
|
104
|
+
13. Where the framework supports a real `config_schema` (raw `NodeExecutor`s), split infra
|
|
105
|
+
concerns (timeout, retries, max_bytes) from business input (query, url, payload).
|
|
106
|
+
`@action`-decorated functions don't get a separate config today — timeout-style fields go on
|
|
107
|
+
the input model instead — but they must still **exist**, not be silently omitted.
|
|
108
|
+
|
|
109
|
+
## 5. Metadata that must not default-drift
|
|
110
|
+
|
|
111
|
+
14. **`determinism` must be accurate**, not left at whatever's convenient: `NON_DETERMINISTIC`
|
|
112
|
+
for anything hitting a live external API/search, `IDEMPOTENT` for side-effect-free GETs,
|
|
113
|
+
`SIDE_EFFECT` for anything that mutates external state or can double-apply on retry, `PURE`
|
|
114
|
+
only for actions with zero external I/O.
|
|
115
|
+
15. **`category` set to the real semantic value** (`IO`, `TOOL`, `DATA`, …), never left at the
|
|
116
|
+
`CUSTOM` default out of not thinking about it.
|
|
117
|
+
16. **`icon` + `tags` set** for palette discoverability; **`description`** on the `@action(...)`
|
|
118
|
+
call itself, not just on individual fields.
|
|
119
|
+
|
|
120
|
+
## 6. Signature & registration safety (already enforced at import time — don't fight it)
|
|
121
|
+
|
|
122
|
+
17. Exact shape: `async def fn(ctx: NodeExecutionContext, inputs: InputModel) -> OutputModel`.
|
|
123
|
+
The decorator's `_validate_action_signature` already rejects non-async functions and the
|
|
124
|
+
`(inputs, ctx)` parameter swap at import time — never route around this by decorating a
|
|
125
|
+
mis-shaped helper.
|
|
126
|
+
18. Unused `ctx`? Discard explicitly with `_ = ctx` — signals "checked, don't need it" instead
|
|
127
|
+
of "forgot."
|
|
128
|
+
19. Dotted, lowercase, namespaced action name matching `ActionSpec.name`'s pattern
|
|
129
|
+
(`^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+$`, e.g. `web.brave.search`) — never a bare function name.
|
|
130
|
+
20. `admin_only=True` on anything with no built-in ownership/RLS scoping (raw SQL, cross-user
|
|
131
|
+
reads, filesystem access) — mirrors the `tool_def.admin_only` gate in matrx-ai.
|
|
132
|
+
|
|
133
|
+
## 7. Fan-out / multi-item actions
|
|
134
|
+
|
|
135
|
+
21. Multi-item fan-outs use `gather_partial` (never a hand-rolled `asyncio.gather` without
|
|
136
|
+
`return_exceptions=True`) — **partial success ships**, one item's failure never destroys its
|
|
137
|
+
siblings' finished work; **total failure raises** via `require_any`. See
|
|
138
|
+
`aidream/graph_actions/_shared.py` (BS-6 doctrine).
|
|
139
|
+
|
|
140
|
+
## 8. Testing
|
|
141
|
+
|
|
142
|
+
22. Every action gets a test on the **failure path**, not just the happy path — assert the
|
|
143
|
+
return is a `Failure` with the documented `code` when the dependency fails. A
|
|
144
|
+
structured-error contract nobody tests is worthless — this is exactly how the Brave bug
|
|
145
|
+
shipped.
|
|
146
|
+
23. Where the dependency is flaky/paid (Brave, SerpAPI, NewsAPI, any billed provider), the
|
|
147
|
+
failure-path test mocks it to return its documented "I failed" sentinel (e.g. `None`) and
|
|
148
|
+
asserts the action does **not** report false success.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Enforcement
|
|
153
|
+
|
|
154
|
+
`scripts/validate_action_standards.py` (repo root, run `--strict` in `release.sh`) walks
|
|
155
|
+
every registered node/action and enforces the Node Result System:
|
|
156
|
+
|
|
157
|
+
- a MIGRATED node (return annotation `NodeResult[...]`) whose payload T carries a reserved
|
|
158
|
+
top-level field (`ok`/`success`/`error`) is a **hard failure**;
|
|
159
|
+
- UNMIGRATED nodes are ratcheted against the list in
|
|
160
|
+
`scripts/node_result_baseline.json` — the list only shrinks
|
|
161
|
+
(`--update-baseline` after migrating); a NEW pre-envelope node **fails the release**.
|
|
162
|
+
|
|
163
|
+
The baseline is empty (ratchet zero, 2026-07-09): the legacy bare-BaseModel branch in
|
|
164
|
+
`matrx_graph/types/result.py::interpret_node_result` (and the scheduler) is DELETED — a bare
|
|
165
|
+
payload return is a hard `ExecutionError`/`TypeError` at dispatch, and the guard is fully strict.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Actions — typed, registered units of work that become graph nodes.
|
|
2
|
+
|
|
3
|
+
Author via::
|
|
4
|
+
|
|
5
|
+
@action(
|
|
6
|
+
name="news_api.top_headlines",
|
|
7
|
+
input_schema=NewsInputs,
|
|
8
|
+
output_schema=NewsOutputs,
|
|
9
|
+
determinism=ActionTier.NON_DETERMINISTIC,
|
|
10
|
+
)
|
|
11
|
+
async def top_headlines(ctx, inputs: NewsInputs) -> NewsOutputs: ...
|
|
12
|
+
|
|
13
|
+
Every decorated function is registered in both the node registry (so the
|
|
14
|
+
scheduler can dispatch ``action.<name>``) and the action registry (so the
|
|
15
|
+
Studio palette can list it and ``tool.call`` can resolve it by name).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from matrx_graph.actions.decorator import (
|
|
19
|
+
ActionSignatureError,
|
|
20
|
+
action,
|
|
21
|
+
iter_registered_action_specs,
|
|
22
|
+
resolve_action_executor,
|
|
23
|
+
)
|
|
24
|
+
from matrx_graph.actions.registry import ActionRegistry, default_action_registry
|
|
25
|
+
from matrx_graph.actions.spec import ActionSpec, build_action_node_spec
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ActionRegistry",
|
|
29
|
+
"ActionSignatureError",
|
|
30
|
+
"ActionSpec",
|
|
31
|
+
"action",
|
|
32
|
+
"build_action_node_spec",
|
|
33
|
+
"default_action_registry",
|
|
34
|
+
"iter_registered_action_specs",
|
|
35
|
+
"resolve_action_executor",
|
|
36
|
+
]
|