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.
Files changed (131) hide show
  1. matrx_graph/__init__.py +236 -0
  2. matrx_graph/actions/ACTION_STANDARDS.md +165 -0
  3. matrx_graph/actions/__init__.py +36 -0
  4. matrx_graph/actions/decorator.py +273 -0
  5. matrx_graph/actions/registry.py +67 -0
  6. matrx_graph/actions/spec.py +160 -0
  7. matrx_graph/checkpoint/__init__.py +37 -0
  8. matrx_graph/checkpoint/memory.py +60 -0
  9. matrx_graph/checkpoint/postgres.py +284 -0
  10. matrx_graph/checkpoint/protocol.py +247 -0
  11. matrx_graph/compile.py +412 -0
  12. matrx_graph/contracts/__init__.py +39 -0
  13. matrx_graph/contracts/audit.py +595 -0
  14. matrx_graph/db/__init__.py +76 -0
  15. matrx_graph/db/_registry.py +105 -0
  16. matrx_graph/db/db_requirements.py +57 -0
  17. matrx_graph/db/definition_store.py +420 -0
  18. matrx_graph/db/event_store.py +221 -0
  19. matrx_graph/db/migrations/0020_wf_run_announce.sql +66 -0
  20. matrx_graph/db/migrations/0021_join_semantics.sql +30 -0
  21. matrx_graph/db/migrations/0022_node_data_slot.sql +109 -0
  22. matrx_graph/db/migrations/0023_fanout_identity.sql +55 -0
  23. matrx_graph/db/migrations/0024_definition_variables.sql +17 -0
  24. matrx_graph/db/migrations/0100_baseline_post_reorg.sql +878 -0
  25. matrx_graph/db/migrations/README.md +89 -0
  26. matrx_graph/db/migrations/legacy_pre_reorg/0001_wf_initial.sql +271 -0
  27. matrx_graph/db/migrations/legacy_pre_reorg/0002_wf_queue.sql +68 -0
  28. matrx_graph/db/migrations/legacy_pre_reorg/0003_wf_node_events.sql +82 -0
  29. matrx_graph/db/migrations/legacy_pre_reorg/0004_wf_trigger.sql +88 -0
  30. matrx_graph/db/migrations/legacy_pre_reorg/0005_wf_rls.sql +211 -0
  31. matrx_graph/db/migrations/legacy_pre_reorg/0006_wf_run_cancelling.sql +25 -0
  32. matrx_graph/db/migrations/legacy_pre_reorg/0007_wf_run_paused_and_errored.sql +36 -0
  33. matrx_graph/db/migrations/legacy_pre_reorg/0008_wf_node_outcome_source.sql +30 -0
  34. matrx_graph/db/migrations/legacy_pre_reorg/0009_wf_run_recovery_limit.sql +42 -0
  35. matrx_graph/db/migrations/legacy_pre_reorg/0010_wf_idempotency.sql +73 -0
  36. matrx_graph/db/migrations/legacy_pre_reorg/0011_wf_definition_concurrency_limit.sql +37 -0
  37. matrx_graph/db/migrations/legacy_pre_reorg/0012_wf_trigger_fire.sql +52 -0
  38. matrx_graph/db/migrations/legacy_pre_reorg/0013_wf_recovery_audit.sql +46 -0
  39. matrx_graph/db/migrations/legacy_pre_reorg/0014_wf_node_events_notify.sql +43 -0
  40. matrx_graph/db/migrations/legacy_pre_reorg/0016_wf_template.sql +62 -0
  41. matrx_graph/db/migrations/legacy_pre_reorg/0017_wf_hardening.sql +27 -0
  42. matrx_graph/db/migrations/legacy_pre_reorg/0018_wf_node_events_seq.sql +62 -0
  43. matrx_graph/db/migrations/legacy_pre_reorg/0019_wf_node_events_drop_check.sql +21 -0
  44. matrx_graph/db/migrations/legacy_pre_reorg/README.md +28 -0
  45. matrx_graph/db/node_data_slot_store.py +312 -0
  46. matrx_graph/db/pool.py +56 -0
  47. matrx_graph/db/run_store.py +625 -0
  48. matrx_graph/db/trigger_store.py +291 -0
  49. matrx_graph/db/wf_manager.py +53 -0
  50. matrx_graph/diagnostics/__init__.py +0 -0
  51. matrx_graph/diagnostics/compile_validation.py +172 -0
  52. matrx_graph/dry_run.py +547 -0
  53. matrx_graph/errors.py +160 -0
  54. matrx_graph/examples/__init__.py +7 -0
  55. matrx_graph/examples/self_validating_news.py +335 -0
  56. matrx_graph/executor/__init__.py +23 -0
  57. matrx_graph/executor/_retry.py +50 -0
  58. matrx_graph/executor/channels.py +151 -0
  59. matrx_graph/executor/introspect.py +57 -0
  60. matrx_graph/executor/json_dump.py +49 -0
  61. matrx_graph/executor/primitives.py +11 -0
  62. matrx_graph/executor/registry.py +142 -0
  63. matrx_graph/executor/scheduler.py +3507 -0
  64. matrx_graph/executor/schema_validation.py +67 -0
  65. matrx_graph/kinds.py +187 -0
  66. matrx_graph/nodes/__init__.py +159 -0
  67. matrx_graph/nodes/_sandbox.py +93 -0
  68. matrx_graph/nodes/control/__init__.py +50 -0
  69. matrx_graph/nodes/control/branch.py +110 -0
  70. matrx_graph/nodes/control/gather.py +194 -0
  71. matrx_graph/nodes/control/human_input.py +108 -0
  72. matrx_graph/nodes/control/loop.py +148 -0
  73. matrx_graph/nodes/control/map.py +162 -0
  74. matrx_graph/nodes/control/send.py +109 -0
  75. matrx_graph/nodes/crypto/__init__.py +25 -0
  76. matrx_graph/nodes/crypto/ops.py +235 -0
  77. matrx_graph/nodes/data/__init__.py +1 -0
  78. matrx_graph/nodes/data/assert_.py +96 -0
  79. matrx_graph/nodes/data/filter.py +92 -0
  80. matrx_graph/nodes/data/json_io.py +135 -0
  81. matrx_graph/nodes/data/merge.py +67 -0
  82. matrx_graph/nodes/data/pick_omit.py +126 -0
  83. matrx_graph/nodes/data/transform.py +91 -0
  84. matrx_graph/nodes/datetime_/__init__.py +21 -0
  85. matrx_graph/nodes/datetime_/ops.py +311 -0
  86. matrx_graph/nodes/http/__init__.py +23 -0
  87. matrx_graph/nodes/http/_shared.py +119 -0
  88. matrx_graph/nodes/http/custom_api.py +294 -0
  89. matrx_graph/nodes/http/get.py +103 -0
  90. matrx_graph/nodes/http/graphql.py +167 -0
  91. matrx_graph/nodes/http/post.py +128 -0
  92. matrx_graph/nodes/io/__init__.py +10 -0
  93. matrx_graph/nodes/io/user_input.py +235 -0
  94. matrx_graph/nodes/output/__init__.py +9 -0
  95. matrx_graph/nodes/output/to_frontend.py +171 -0
  96. matrx_graph/nodes/subgraph/__init__.py +5 -0
  97. matrx_graph/nodes/subgraph/call.py +162 -0
  98. matrx_graph/nodes/text/__init__.py +37 -0
  99. matrx_graph/nodes/text/custom_extract.py +387 -0
  100. matrx_graph/nodes/text/json_path.py +156 -0
  101. matrx_graph/nodes/text/manipulate.py +401 -0
  102. matrx_graph/nodes/text/regex.py +271 -0
  103. matrx_graph/nodes/tool/__init__.py +5 -0
  104. matrx_graph/nodes/tool/call.py +163 -0
  105. matrx_graph/observability/__init__.py +117 -0
  106. matrx_graph/py.typed +0 -0
  107. matrx_graph/references.py +113 -0
  108. matrx_graph/types/__init__.py +107 -0
  109. matrx_graph/types/canonical_payloads.py +138 -0
  110. matrx_graph/types/channel.py +73 -0
  111. matrx_graph/types/compiled.py +149 -0
  112. matrx_graph/types/context.py +225 -0
  113. matrx_graph/types/definition.py +240 -0
  114. matrx_graph/types/events.py +424 -0
  115. matrx_graph/types/handle.py +39 -0
  116. matrx_graph/types/node_spec.py +209 -0
  117. matrx_graph/types/primitives.py +117 -0
  118. matrx_graph/types/result.py +133 -0
  119. matrx_graph/types/run.py +39 -0
  120. matrx_graph/types/send.py +59 -0
  121. matrx_graph/types/usl.py +112 -0
  122. matrx_graph/types/value_objects.py +36 -0
  123. matrx_graph/types/variables.py +143 -0
  124. matrx_graph/validation.py +559 -0
  125. matrx_graph/workers/__init__.py +62 -0
  126. matrx_graph/workers/cron_watcher.py +100 -0
  127. matrx_graph/workers/queue.py +360 -0
  128. matrx_graph/workers/runner.py +164 -0
  129. matrx_graph-0.1.0.dist-info/METADATA +110 -0
  130. matrx_graph-0.1.0.dist-info/RECORD +131 -0
  131. matrx_graph-0.1.0.dist-info/WHEEL +4 -0
@@ -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
+ ]