graphrefly 0.19.0__tar.gz → 0.20.0__tar.gz

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 (310) hide show
  1. {graphrefly-0.19.0 → graphrefly-0.20.0}/CHANGELOG.md +16 -0
  2. {graphrefly-0.19.0 → graphrefly-0.20.0}/CLAUDE.md +13 -0
  3. {graphrefly-0.19.0 → graphrefly-0.20.0}/PKG-INFO +21 -4
  4. {graphrefly-0.19.0 → graphrefly-0.20.0}/README.md +20 -3
  5. graphrefly-0.20.0/llms.txt +114 -0
  6. {graphrefly-0.19.0 → graphrefly-0.20.0}/pyproject.toml +1 -1
  7. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/tier1.py +23 -9
  8. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/tier2.py +6 -24
  9. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/ai.py +118 -3
  10. graphrefly-0.20.0/src/graphrefly/patterns/harness/bridge.py +473 -0
  11. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_tier1.py +45 -0
  12. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_tier2.py +94 -22
  13. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_harness.py +387 -0
  14. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/astro.config.mjs +4 -0
  15. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/Header.astro +1 -0
  16. graphrefly-0.19.0/llms.txt +0 -272
  17. graphrefly-0.19.0/src/graphrefly/patterns/harness/bridge.py +0 -161
  18. {graphrefly-0.19.0 → graphrefly-0.20.0}/.github/workflows/pages.yml +0 -0
  19. {graphrefly-0.19.0 → graphrefly-0.20.0}/.github/workflows/release.yml +0 -0
  20. {graphrefly-0.19.0 → graphrefly-0.20.0}/.gitignore +0 -0
  21. {graphrefly-0.19.0 → graphrefly-0.20.0}/.mise.toml +0 -0
  22. {graphrefly-0.19.0 → graphrefly-0.20.0}/CONTRIBUTING.md +0 -0
  23. {graphrefly-0.19.0 → graphrefly-0.20.0}/GEMINI.md +0 -0
  24. {graphrefly-0.19.0 → graphrefly-0.20.0}/LICENSE +0 -0
  25. {graphrefly-0.19.0 → graphrefly-0.20.0}/benchmarks/py-baseline.json +0 -0
  26. {graphrefly-0.19.0 → graphrefly-0.20.0}/docs/ADAPTER-CONTRACT.md +0 -0
  27. {graphrefly-0.19.0 → graphrefly-0.20.0}/docs/benchmark.md +0 -0
  28. {graphrefly-0.19.0 → graphrefly-0.20.0}/examples/README.md +0 -0
  29. {graphrefly-0.19.0 → graphrefly-0.20.0}/examples/basic_counter.py +0 -0
  30. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/__init__.py +0 -0
  31. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/compat/__init__.py +0 -0
  32. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/compat/async_utils.py +0 -0
  33. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/compat/asyncio_runner.py +0 -0
  34. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/compat/trio_runner.py +0 -0
  35. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/__init__.py +0 -0
  36. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/bridge.py +0 -0
  37. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/cancellation.py +0 -0
  38. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/clock.py +0 -0
  39. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/dynamic_node.py +0 -0
  40. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/guard.py +0 -0
  41. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/meta.py +0 -0
  42. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/node.py +0 -0
  43. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/node_base.py +0 -0
  44. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/protocol.py +0 -0
  45. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/runner.py +0 -0
  46. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/subgraph_locks.py +0 -0
  47. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/sugar.py +0 -0
  48. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/timer.py +0 -0
  49. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/versioning.py +0 -0
  50. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/__init__.py +0 -0
  51. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/adapters.py +0 -0
  52. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/backoff.py +0 -0
  53. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/backpressure.py +0 -0
  54. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/cascading_cache.py +0 -0
  55. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/checkpoint.py +0 -0
  56. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/composite.py +0 -0
  57. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/cron.py +0 -0
  58. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/data_structures.py +0 -0
  59. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/resilience.py +0 -0
  60. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/sources.py +0 -0
  61. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/__init__.py +0 -0
  62. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/codec.py +0 -0
  63. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/graph.py +0 -0
  64. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/profile.py +0 -0
  65. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/sizeof.py +0 -0
  66. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/integrations/__init__.py +0 -0
  67. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/integrations/django.py +0 -0
  68. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/integrations/fastapi.py +0 -0
  69. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/__init__.py +0 -0
  70. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/_internal.py +0 -0
  71. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/cqrs.py +0 -0
  72. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/domain_templates.py +0 -0
  73. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/graphspec.py +0 -0
  74. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/__init__.py +0 -0
  75. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/loop.py +0 -0
  76. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/profile.py +0 -0
  77. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/strategy.py +0 -0
  78. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/trace.py +0 -0
  79. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/types.py +0 -0
  80. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/memory.py +0 -0
  81. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/messaging.py +0 -0
  82. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/orchestration.py +0 -0
  83. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reactive_layout/__init__.py +0 -0
  84. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reactive_layout/measurement_adapters.py +0 -0
  85. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reactive_layout/reactive_block_layout.py +0 -0
  86. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reactive_layout/reactive_layout.py +0 -0
  87. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reduction.py +0 -0
  88. {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/py.typed +0 -0
  89. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/__init__.py +0 -0
  90. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/bench_core.py +0 -0
  91. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/conftest.py +0 -0
  92. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/helpers/__init__.py +0 -0
  93. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/helpers/mock_llm.py +0 -0
  94. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_adapter_contract.py +0 -0
  95. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_adapters_ingest.py +0 -0
  96. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_adapters_storage.py +0 -0
  97. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_backpressure.py +0 -0
  98. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_bridge.py +0 -0
  99. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_cascading_cache.py +0 -0
  100. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_concurrency.py +0 -0
  101. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_core.py +0 -0
  102. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_django.py +0 -0
  103. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_domain_templates.py +0 -0
  104. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_dynamic_node.py +0 -0
  105. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_edge_cases.py +0 -0
  106. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_composite.py +0 -0
  107. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_data_structures.py +0 -0
  108. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_resilience.py +0 -0
  109. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_sources.py +0 -0
  110. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_sources_http.py +0 -0
  111. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_fastapi.py +0 -0
  112. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_graph.py +0 -0
  113. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_graphspec.py +0 -0
  114. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_guard.py +0 -0
  115. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_measurement_adapters.py +0 -0
  116. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_operator_protocol_matrix.py +0 -0
  117. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_ai.py +0 -0
  118. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_cqrs.py +0 -0
  119. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_memory.py +0 -0
  120. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_messaging.py +0 -0
  121. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_orchestration.py +0 -0
  122. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_reduction.py +0 -0
  123. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_perf_smoke.py +0 -0
  124. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_protocol.py +0 -0
  125. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_reactive_block_layout.py +0 -0
  126. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_reactive_layout.py +0 -0
  127. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_reduction.py +0 -0
  128. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_regressions.py +0 -0
  129. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_runner.py +0 -0
  130. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_smoke.py +0 -0
  131. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_sugar.py +0 -0
  132. {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_versioning.py +0 -0
  133. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/.gitignore +0 -0
  134. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/README.md +0 -0
  135. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/content.config.ts +0 -0
  136. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/package.json +0 -0
  137. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/pnpm-lock.yaml +0 -0
  138. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/public/llms.txt +0 -0
  139. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/py-api-sidebar.mjs +0 -0
  140. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/scripts/gen_api_docs.py +0 -0
  141. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/scripts/sync-docs.mjs +0 -0
  142. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/GraphreflyHero.astro +0 -0
  143. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/MobileMenuFooter.astro +0 -0
  144. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/PyodidePlayground.tsx +0 -0
  145. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/Sidebar.astro +0 -0
  146. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/SiteTitle.astro +0 -0
  147. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/BackoffPreset.md +0 -0
  148. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/BackoffStrategy.md +0 -0
  149. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/CheckpointAdapter.md +0 -0
  150. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/CircuitBreaker.md +0 -0
  151. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/CircuitOpenError.md +0 -0
  152. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/CompactEntry.md +0 -0
  153. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/DeferWhen.md +0 -0
  154. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/DictCheckpointAdapter.md +0 -0
  155. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/DistillBundle.md +0 -0
  156. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/DownStrategy.md +0 -0
  157. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/Extraction.md +0 -0
  158. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/FileCheckpointAdapter.md +0 -0
  159. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/HttpBundle.md +0 -0
  160. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/JitterMode.md +0 -0
  161. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/MemoryCheckpointAdapter.md +0 -0
  162. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/Message.md +0 -0
  163. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/MessageType.md +0 -0
  164. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/Messages.md +0 -0
  165. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/NodeActions.md +0 -0
  166. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/NodeFn.md +0 -0
  167. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/NodeImpl.md +0 -0
  168. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/NodeStatus.md +0 -0
  169. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/PipeOperator.md +0 -0
  170. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/PubSubHub.md +0 -0
  171. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveCounterBundle.md +0 -0
  172. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveIndexBundle.md +0 -0
  173. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveListBundle.md +0 -0
  174. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveLogBundle.md +0 -0
  175. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveMapBundle.md +0 -0
  176. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/SqliteCheckpointAdapter.md +0 -0
  177. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/SubscribeHints.md +0 -0
  178. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/TimeoutError.md +0 -0
  179. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/TokenBucket.md +0 -0
  180. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/VerifiableBundle.md +0 -0
  181. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/Versioned.md +0 -0
  182. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/WithBreakerBundle.md +0 -0
  183. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/WithStatusBundle.md +0 -0
  184. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/audit.md +0 -0
  185. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/batch.md +0 -0
  186. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/buffer.md +0 -0
  187. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/buffer_count.md +0 -0
  188. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/buffer_time.md +0 -0
  189. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/cache.md +0 -0
  190. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/cached.md +0 -0
  191. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/checkpoint_node_value.md +0 -0
  192. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/circuit_breaker.md +0 -0
  193. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/combine.md +0 -0
  194. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/concat.md +0 -0
  195. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/concat_map.md +0 -0
  196. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/constant.md +0 -0
  197. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/debounce.md +0 -0
  198. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/decorrelated_jitter.md +0 -0
  199. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/delay.md +0 -0
  200. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/derived.md +0 -0
  201. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/dispatch_messages.md +0 -0
  202. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/distill.md +0 -0
  203. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/distinct_until_changed.md +0 -0
  204. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/down_with_batch.md +0 -0
  205. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/effect.md +0 -0
  206. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/element_at.md +0 -0
  207. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/empty.md +0 -0
  208. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/exhaust_map.md +0 -0
  209. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/exponential.md +0 -0
  210. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/fallback.md +0 -0
  211. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/fibonacci.md +0 -0
  212. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/filter.md +0 -0
  213. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/find.md +0 -0
  214. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/first.md +0 -0
  215. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/first_value_from.md +0 -0
  216. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/flat_map.md +0 -0
  217. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/for_each.md +0 -0
  218. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_any.md +0 -0
  219. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_async_iter.md +0 -0
  220. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_awaitable.md +0 -0
  221. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_cron.md +0 -0
  222. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_event_emitter.md +0 -0
  223. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_fs_watch.md +0 -0
  224. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_git_hook.md +0 -0
  225. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_http.md +0 -0
  226. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_iter.md +0 -0
  227. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_mcp.md +0 -0
  228. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_timer.md +0 -0
  229. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_webhook.md +0 -0
  230. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_websocket.md +0 -0
  231. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/gate.md +0 -0
  232. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/index.md +0 -0
  233. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/interval.md +0 -0
  234. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/is_batching.md +0 -0
  235. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/is_local_only.md +0 -0
  236. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/is_phase2_message.md +0 -0
  237. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/is_terminal_message.md +0 -0
  238. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/keepalive.md +0 -0
  239. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/last.md +0 -0
  240. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/linear.md +0 -0
  241. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/log_slice.md +0 -0
  242. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/map.md +0 -0
  243. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/merge.md +0 -0
  244. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/message_tier.md +0 -0
  245. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/never.md +0 -0
  246. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/node.md +0 -0
  247. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/of.md +0 -0
  248. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/operator.md +0 -0
  249. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/pairwise.md +0 -0
  250. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/partition_for_batch.md +0 -0
  251. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/pausable.md +0 -0
  252. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/pipe.md +0 -0
  253. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/producer.md +0 -0
  254. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/propagates_to_meta.md +0 -0
  255. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/pubsub.md +0 -0
  256. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/race.md +0 -0
  257. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/rate_limiter.md +0 -0
  258. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_counter.md +0 -0
  259. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_index.md +0 -0
  260. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_list.md +0 -0
  261. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_log.md +0 -0
  262. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_map.md +0 -0
  263. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reduce.md +0 -0
  264. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/repeat.md +0 -0
  265. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/replay.md +0 -0
  266. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/rescue.md +0 -0
  267. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/resolve_backoff_preset.md +0 -0
  268. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/restore_graph_checkpoint.md +0 -0
  269. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/retry.md +0 -0
  270. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/sample.md +0 -0
  271. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/save_graph_checkpoint.md +0 -0
  272. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/scan.md +0 -0
  273. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/share.md +0 -0
  274. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/skip.md +0 -0
  275. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/start_with.md +0 -0
  276. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/state.md +0 -0
  277. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/subscribe.md +0 -0
  278. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/switch_map.md +0 -0
  279. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/take.md +0 -0
  280. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/take_until.md +0 -0
  281. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/take_while.md +0 -0
  282. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/tap.md +0 -0
  283. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/throttle.md +0 -0
  284. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/throw_error.md +0 -0
  285. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/timeout.md +0 -0
  286. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/timeout_node.md +0 -0
  287. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/to_array.md +0 -0
  288. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/to_list.md +0 -0
  289. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/to_sse.md +0 -0
  290. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/to_websocket.md +0 -0
  291. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/token_bucket.md +0 -0
  292. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/token_tracker.md +0 -0
  293. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/valve.md +0 -0
  294. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/verifiable.md +0 -0
  295. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/window.md +0 -0
  296. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/window_count.md +0 -0
  297. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/window_time.md +0 -0
  298. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/with_breaker.md +0 -0
  299. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/with_latest_from.md +0 -0
  300. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/with_max_attempts.md +0 -0
  301. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/with_status.md +0 -0
  302. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/zip.md +0 -0
  303. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/index.mdx +0 -0
  304. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/lab/python.mdx +0 -0
  305. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/recipes/index.md +0 -0
  306. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content.config.ts +0 -0
  307. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/env.d.ts +0 -0
  308. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/styles/custom.css +0 -0
  309. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/theme-prototypes.html +0 -0
  310. {graphrefly-0.19.0 → graphrefly-0.20.0}/website/tsconfig.json +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.20.0 (2026-04-11)
6
+
7
+ ### Chores
8
+
9
+ - Add integrations page
10
+ ([`7f16d1b`](https://github.com/graphrefly/graphrefly-py/commit/7f16d1b9d9b5aadb5875e4bcb08e6decae469d1f))
11
+
12
+ - Update docs
13
+ ([`0f2d779`](https://github.com/graphrefly/graphrefly-py/commit/0f2d779eeddcf9eec9ed339bcae4dbf6899363d3))
14
+
15
+ ### Features
16
+
17
+ - Fix bug
18
+ ([`e5f9cc4`](https://github.com/graphrefly/graphrefly-py/commit/e5f9cc440a854cde0aac1060f02121b85ce06831))
19
+
20
+
5
21
  ## v0.19.0 (2026-04-10)
6
22
 
7
23
  ### Chores
@@ -14,6 +14,16 @@ uv workspace managed by mise. `mise trust && mise install` to set up uv. `uv syn
14
14
  - Format: `uv run ruff format src/ tests/`
15
15
  - Type check: `uv run mypy src/`
16
16
 
17
+ ## Documentation workflow (critical)
18
+
19
+ - Follow cross-language standard in `~/src/graphrefly-ts/docs/docs-guidance.md`.
20
+ - `website/src/content/docs/api/*.md` pages are generated; do not hand-edit.
21
+ - For docs updates in this repo:
22
+ 1. Update source docstrings.
23
+ 2. Run `pnpm --dir website docs:gen`.
24
+ 3. Validate with `pnpm --dir website docs:gen:check` and `pnpm --dir website sync-docs:check`.
25
+ - Keep `llms.txt` concise and source-oriented; avoid long static API inventories that drift.
26
+
17
27
  ## Package naming
18
28
 
19
29
  - Distribution name: `graphrefly-py`
@@ -35,6 +45,9 @@ uv workspace managed by mise. `mise trust && mise install` to set up uv. `uv syn
35
45
  | Behavior spec | `~/src/graphrefly/GRAPHREFLY-SPEC.md` |
36
46
  | Composition guide | `~/src/graphrefly/COMPOSITION-GUIDE.md` |
37
47
  | Roadmap, optimizations, test/docs guidance, skills, archive | `~/src/graphrefly-ts/` (single source of truth) |
48
+ | Reactive collaboration harness (§9.0) — 7-stage loop, gate, `promptNode`, strategy model | `~/src/graphrefly-ts/archive/docs/SESSION-reactive-collaboration-harness.md` |
49
+ | Harness engineering strategy — 8 requirements, wave plan, MCP priority | `~/src/graphrefly-ts/archive/docs/SESSION-harness-engineering-strategy.md` |
50
+ | Marketing & positioning — pillars, wave plan, reply playbooks, blog plan | `~/src/graphrefly-ts/archive/docs/SESSION-marketing-promotion-strategy.md` |
38
51
  | Predecessor (patterns, concurrency) | `~/src/callbag-recharge-py` (reference only, not spec) |
39
52
 
40
53
  ## Design invariants (spec §5.8–5.12)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphrefly
3
- Version: 0.19.0
3
+ Version: 0.20.0
4
4
  Summary: Reactive harness layer for agent workflows. Describe automations in plain language, trace every decision, enforce policies, persist checkpoints. Zero dependencies.
5
5
  Project-URL: Homepage, https://py.graphrefly.dev
6
6
  Project-URL: Repository, https://github.com/graphrefly/graphrefly-py
@@ -25,15 +25,15 @@ Description-Content-Type: text/markdown
25
25
 
26
26
  # GraphReFly
27
27
 
28
- **Describe what matters. It watches, filters, and explains — persistently.**
28
+ **The reactive harness layer for agent workflows.** Describe in plain language, review visually, run persistently, trace every decision.
29
29
 
30
- You're buried under emails, alerts, feeds, and messages. You can't process it all. GraphReFly lets you describe automations in plain language, review them visually, run them persistently, and trace every decision back to its source.
30
+ GraphReFly makes long-running human + LLM co-operation reactive, resumable, and causally explainable. State pushes downstream on change (no re-reading), nodes have lifecycles (not infinite append), and every decision has a traceable causal chain the substrate underneath tools, agents, and personal automations.
31
31
 
32
32
  [![PyPI](https://img.shields.io/pypi/v/graphrefly?color=blue)](https://pypi.org/project/graphrefly/)
33
33
  [![license](https://img.shields.io/github/license/graphrefly/graphrefly-py)](./LICENSE)
34
34
  [![Python](https://img.shields.io/pypi/pyversions/graphrefly)](https://pypi.org/project/graphrefly/)
35
35
 
36
- [Docs](https://py.graphrefly.dev) | [Spec](https://py.graphrefly.dev/spec/) | [TypeScript](https://graphrefly.dev) | [API Reference](https://py.graphrefly.dev/api/)
36
+ [Docs](https://graphrefly.dev/py/) | [Spec](https://graphrefly.dev/spec/) | [TypeScript](https://graphrefly.dev) | [Python API](https://graphrefly.dev/py/api/)
37
37
 
38
38
  ---
39
39
 
@@ -72,6 +72,23 @@ count.push(3)
72
72
 
73
73
  You describe what you need — an LLM composes a reactive graph (like SQL for data flows). The graph runs persistently, checkpoints its state, and traces every decision through a causal chain. Ask "why?" at any point and get a human-readable explanation from source to conclusion.
74
74
 
75
+ ## Harness engineering coverage
76
+
77
+ The eight requirements of a production agent harness cluster into a handful of composed blocks that sit on top of the reactive graph primitives:
78
+
79
+ | Need | GraphReFly |
80
+ |---|---|
81
+ | Context & state | `persistent_state()` — `auto_checkpoint` + `snapshot` / `restore` + incremental diff |
82
+ | Agent memory | `agent_memory()` — `distill` + vectors + knowledge graph + tiers, OpenViking decay |
83
+ | Control flow & resilience | `resilient_pipeline()` — `rate_limiter → breaker → retry → timeout → fallback`, correct ordering built in |
84
+ | Execution & policy | `guarded_execution()` — Actor / Guard ABAC + `policy()` + `budget_gate` + scoped describe |
85
+ | Observability & causality | `graph_lens()` — reactive topology, health, flow, and `why(node)` causal chains as structured data |
86
+ | Human governance | `gate` — reactive `pending` / `is_open` with `approve` / `reject` / `modify(fn, n)` |
87
+ | Verification | Multi-model eval harness with regression gates |
88
+ | Continuous improvement | Strategy model: `root_cause × intervention → success_rate` |
89
+
90
+ The library computes structured facts reactively; LLMs and UIs render them. Natural language is never the library's job — which keeps the whole stack model-agnostic and testable.
91
+
75
92
  ## Why GraphReFly?
76
93
 
77
94
  | | Redux / Zustand | RxPY | Pydantic AI | LangGraph | TC39 Signals | **GraphReFly** |
@@ -1,14 +1,14 @@
1
1
  # GraphReFly
2
2
 
3
- **Describe what matters. It watches, filters, and explains — persistently.**
3
+ **The reactive harness layer for agent workflows.** Describe in plain language, review visually, run persistently, trace every decision.
4
4
 
5
- You're buried under emails, alerts, feeds, and messages. You can't process it all. GraphReFly lets you describe automations in plain language, review them visually, run them persistently, and trace every decision back to its source.
5
+ GraphReFly makes long-running human + LLM co-operation reactive, resumable, and causally explainable. State pushes downstream on change (no re-reading), nodes have lifecycles (not infinite append), and every decision has a traceable causal chain the substrate underneath tools, agents, and personal automations.
6
6
 
7
7
  [![PyPI](https://img.shields.io/pypi/v/graphrefly?color=blue)](https://pypi.org/project/graphrefly/)
8
8
  [![license](https://img.shields.io/github/license/graphrefly/graphrefly-py)](./LICENSE)
9
9
  [![Python](https://img.shields.io/pypi/pyversions/graphrefly)](https://pypi.org/project/graphrefly/)
10
10
 
11
- [Docs](https://py.graphrefly.dev) | [Spec](https://py.graphrefly.dev/spec/) | [TypeScript](https://graphrefly.dev) | [API Reference](https://py.graphrefly.dev/api/)
11
+ [Docs](https://graphrefly.dev/py/) | [Spec](https://graphrefly.dev/spec/) | [TypeScript](https://graphrefly.dev) | [Python API](https://graphrefly.dev/py/api/)
12
12
 
13
13
  ---
14
14
 
@@ -47,6 +47,23 @@ count.push(3)
47
47
 
48
48
  You describe what you need — an LLM composes a reactive graph (like SQL for data flows). The graph runs persistently, checkpoints its state, and traces every decision through a causal chain. Ask "why?" at any point and get a human-readable explanation from source to conclusion.
49
49
 
50
+ ## Harness engineering coverage
51
+
52
+ The eight requirements of a production agent harness cluster into a handful of composed blocks that sit on top of the reactive graph primitives:
53
+
54
+ | Need | GraphReFly |
55
+ |---|---|
56
+ | Context & state | `persistent_state()` — `auto_checkpoint` + `snapshot` / `restore` + incremental diff |
57
+ | Agent memory | `agent_memory()` — `distill` + vectors + knowledge graph + tiers, OpenViking decay |
58
+ | Control flow & resilience | `resilient_pipeline()` — `rate_limiter → breaker → retry → timeout → fallback`, correct ordering built in |
59
+ | Execution & policy | `guarded_execution()` — Actor / Guard ABAC + `policy()` + `budget_gate` + scoped describe |
60
+ | Observability & causality | `graph_lens()` — reactive topology, health, flow, and `why(node)` causal chains as structured data |
61
+ | Human governance | `gate` — reactive `pending` / `is_open` with `approve` / `reject` / `modify(fn, n)` |
62
+ | Verification | Multi-model eval harness with regression gates |
63
+ | Continuous improvement | Strategy model: `root_cause × intervention → success_rate` |
64
+
65
+ The library computes structured facts reactively; LLMs and UIs render them. Natural language is never the library's job — which keeps the whole stack model-agnostic and testable.
66
+
50
67
  ## Why GraphReFly?
51
68
 
52
69
  | | Redux / Zustand | RxPY | Pydantic AI | LangGraph | TC39 Signals | **GraphReFly** |
@@ -0,0 +1,114 @@
1
+ # GraphReFly — Python package (`graphrefly`)
2
+
3
+ The reactive harness layer for agent workflows. Reactive graph runtime for human + LLM co-operation — causal tracing, persistent checkpoints, policy enforcement, zero runtime dependencies.
4
+
5
+ Keywords: harness engineering, agent harness, reactive graph, causal trace, explain_path, agent orchestration, LLM co-operation, human-in-the-loop, reactive middleware, universal reduction layer.
6
+
7
+ This file is an AI-oriented index for the Python repo. It points to source-of-truth docs and generated API references instead of maintaining a long, drift-prone inline API catalog.
8
+
9
+ ## Canonical behavior and docs authority
10
+
11
+ - Behavior spec: `~/src/graphrefly/GRAPHREFLY-SPEC.md`
12
+ - Composition guidance: `~/src/graphrefly/COMPOSITION-GUIDE.md`
13
+ - Docs conventions (cross-language): `~/src/graphrefly-ts/docs/docs-guidance.md`
14
+ - Roadmap/state (cross-language): `~/src/graphrefly-ts/docs/roadmap.md`
15
+ - Testing standard (cross-language): `~/src/graphrefly-ts/docs/test-guidance.md`
16
+
17
+ ## Active strategy docs (cross-language, live in graphrefly-ts)
18
+
19
+ - `~/src/graphrefly-ts/archive/docs/SESSION-reactive-collaboration-harness.md` — 7-stage reactive collaboration loop, gate port, `prompt_node`, `valve` rename, strategy model, `harness_loop()` factory
20
+ - `~/src/graphrefly-ts/archive/docs/SESSION-harness-engineering-strategy.md` — 8-requirement coverage, three-wave announcement plan, MCP Server priority, eval system design
21
+ - `~/src/graphrefly-ts/archive/docs/SESSION-marketing-promotion-strategy.md` — positioning pillars, wave plan, reply-marketing playbooks, competitive intel, prompt optimization algorithms, blog content plan
22
+
23
+ ## Primary links
24
+
25
+ - Python docs: https://graphrefly.dev/py/
26
+ - Python API reference: https://graphrefly.dev/py/api/
27
+ - TypeScript docs: https://graphrefly.dev
28
+
29
+ ## Package entry points
30
+
31
+ - Root: `from graphrefly import ...`
32
+ - Subpackages: `graphrefly.core`, `graphrefly.graph`, `graphrefly.extra`, `graphrefly.patterns`, `graphrefly.compat`, `graphrefly.integrations`
33
+
34
+ ## Public API by subpackage
35
+
36
+ Full export lists live in the generated API pages under `website/src/content/docs/api/`. The descriptions below explain *what lives in each subpackage and why*, so an LLM reading this cold can navigate without consuming the drift-prone catalog. Python uses snake_case (`from_timer`, `switch_map`, `agent_loop`) — the behavior parity with TypeScript holds, the naming does not.
37
+
38
+ ### `graphrefly.core` — `src/graphrefly/core/`
39
+
40
+ The minimal reactive substrate. One primitive (`node`) plus sugar constructors (`state`, `derived`, `producer`, `effect`, `pipe`, `dynamic_node`). Protocol internals: `MessageType` enum (`DATA`, `DIRTY`, `RESOLVED`, `COMPLETE`, `ERROR`, `TEARDOWN`, `INVALIDATE`, `PAUSE`, `RESUME`), dispatch (`dispatch_messages`, `is_phase2_message`), two-phase batch semantics (`batch`, `is_batching`, `partition_for_batch`, `down_with_batch`), guard / policy (`policy`, `policy_from_rules`, `compose_guards`, `GuardDenied`, `access_hint_for_guard`), actor identity (`Actor`, `system_actor`, `normalize_actor`), meta snapshots (`meta_snapshot`, `describe_node`), versioning (`create_versioning`, `advance_version`, `default_hash`, `is_v1`), and the central clock (`monotonic_ns`, `wall_clock_ns`). **Anything outside this package must not call `time.time_ns()` / `time.monotonic_ns()` directly — use `src/graphrefly/core/clock.py`.** Also holds the `Runner` protocol and default-runner resolution (`get_default_runner`, `set_default_runner`, `resolve_runner`) for asyncio/trio backends. Thread-safety invariants (per-subgraph `RLock`, per-node `_cache_lock`) are enforced here.
41
+
42
+ ### `graphrefly.graph` — `src/graphrefly/graph/`
43
+
44
+ The `Graph` container: reactive node registry with `register` / `resolve` / `describe` / `observe` / `snapshot` / `diff` / `diagram` / `spy` / `mount` / `unmount` / `teardown`. `reachable(graph, start_path, options=None)` computes the dependency closure from a node path — foundation for `explain_path` (causal walkback) and persistence scope.
45
+
46
+ ### `graphrefly.extra` — `src/graphrefly/extra/`
47
+
48
+ Everything reactive that isn't the primitive. Organized into focused modules:
49
+
50
+ - **Tier 1 operators** (`tier1.py`) — transform (`map`, `filter`, `scan`, `reduce`), limiting (`take`, `skip`, `take_while`, `take_until`), selection (`first`, `last`, `find`, `element_at`), utility (`start_with`, `tap`, `distinct_until_changed`, `pairwise`), combination (`combine`, `with_latest_from`), merging (`merge`, `zip`, `concat`, `race`).
51
+ - **Tier 2 operators** (`tier2.py`) — higher-order (`switch_map`, `concat_map`, `flat_map`, `exhaust_map`), timing (`debounce`, `throttle`, `delay`), sampling (`sample`, `audit`, `timeout`), buffering (`buffer`, `buffer_count`, `buffer_time`), windowing (`window`, `window_count`, `window_time`), `interval`, `repeat`, `pausable`, `rescue`. **Note:** the existing `gate(control)` here is a boolean control gate and is being renamed to `valve` per §9.0; the new human-approval `gate` lives under `patterns.orchestration`.
52
+ - **Sources** (`sources.py`) — synchronous (`of`, `empty`, `never`, `throw_error`, `from_iter`, `from_timer`, `from_cron`), async bridges (`from_awaitable`, `from_async_iter`), network / IO (`from_http`, `from_event_emitter`, `from_fs_watch`, `from_webhook`, `from_websocket`, `from_mcp`, `from_git_hook`), multicast / caching (`share`, `cached`, `replay`), collectors (`for_each`, `to_list`, `to_array`, `first_value_from`), sinks (`to_sse`, `to_websocket`). **All async boundaries must enter through sources** — node fns and operators never contain `async def` or raw awaitables.
53
+ - **Data structures** (`data_structures.py` + dedicated files) — `reactive_map`, `reactive_log` + `log_slice`, `reactive_index`, `reactive_list`, `pubsub`.
54
+ - **Resilience** (`resilience.py` + `backoff.py`) — `retry`, `circuit_breaker` + `CircuitOpenError`, `token_bucket`, `token_tracker`, `rate_limiter`, `with_breaker`, `with_status`; backoff strategies (`constant`, `linear`, `exponential`, `fibonacci`, `decorrelated_jitter`, `with_max_attempts`, `resolve_backoff_preset`).
55
+ - **Cron** (`cron.py`) — `parse_cron`, `matches_cron`.
56
+ - **Checkpoint** (`checkpoint.py`) — `Memory` / `Dict` / `File` / `Sqlite` adapters, `save_graph_checkpoint` / `restore_graph_checkpoint`, `checkpoint_node_value`. Auto-checkpoint is gated by `message_tier >= 3`. See `docs/ADAPTER-CONTRACT.md` for the adapter contract.
57
+ - **Composite** (`composite.py`) — `verifiable(source, opts)` wraps a node with verification tracking; `distill(raw, extract, opts)` compacts raw data into a budget-bounded summary (the reactive memory primitive that `agent_memory` plugs into).
58
+ - **Backpressure** (`backpressure.py`) — watermark controller for PAUSE/RESUME flow control.
59
+
60
+ ### `graphrefly.patterns` — `src/graphrefly/patterns/` (Phase 4+ domain APIs)
61
+
62
+ Developer-facing factories that compose primitives into recognizable patterns. **Protocol internals never surface here** — sensible defaults, minimal boilerplate, clear errors. Python uses `with batch():` context managers instead of TypeScript's callback form, and `Node.__or__` maps to the TS `pipe()`.
63
+
64
+ - **Orchestration** (`orchestration.py`) — `pipeline`, `task`, `branch`, the boolean control `gate` *(being renamed to `valve`)*, `approval`, `for_each`, `join`, `loop`, `sub_pipeline`, `sensor`, `wait`, `on_failure`. The §9.0 port of the human-approval gate (`pending` / `count` / `is_open` reactive nodes and `approve` / `reject` / `modify(fn, n=1)` / `open` / `close` methods) also lands here.
65
+ - **Messaging** (`messaging.py`) — `topic` (TopicGraph), `subscription` (SubscriptionGraph with cursor / `pull` / `ack`), `job_queue`, `job_flow`, `topic_bridge`. These are the primitives the §9.0 harness loop uses for static-topology sinks and cursor-driven readers.
66
+ - **Memory** (`memory.py`) — `collection`, `light_collection`, `vector_index`, `knowledge_graph`, `decay(source, opts)` (OpenViking exponential decay, 7-day default half-life). `decay` is the priority-scoring primitive for the §9.0 QUEUE stage.
67
+ - **AI** (`ai.py`) — `from_llm`, `streaming_prompt_node`, `stream_extractor`, `chat_stream`, `tool_registry`, `system_prompt_builder`, `llm_extractor`, `llm_consolidator`, `agent_memory` (default: distill + vectors + KG + tiers), `agent_loop` (ReAct inner loop), `knobs_as_tools`, `gauges_as_context`, `validate_graph_def`. The §9.0 `prompt_node` factory (wraps any LLM call in a derived node with retry / cache / structured output) also lives here and is what plugs into `distill` as `extract_fn` / `consolidate_fn`.
68
+ - **CQRS** (`cqrs.py`) — `cqrs(name, opts=None)` creates a commands / events / projections / sagas Graph.
69
+ - **Reactive layout** (`reactive_layout/`) — `reactive_layout`, `reactive_block_layout`, `analyze_and_measure`, `compute_line_breaks`, `compute_char_positions`. DOM-free Knuth-Plass line breaking as a `state → derived` graph.
70
+
71
+ ### `graphrefly.compat` — `src/graphrefly/compat/`
72
+
73
+ Async runner bindings. `AsyncioRunner` wraps a Graph in an asyncio event loop; `TrioRunner` does the same for trio. These are the only places allowed to orchestrate coroutines — node fns and operators stay synchronous. Runners resolve via `graphrefly.core.resolve_runner` so downstream code can inject a specific backend.
74
+
75
+ ### `graphrefly.integrations` — `src/graphrefly/integrations/`
76
+
77
+ Framework integrations. `fastapi.GraphReflyRouter(graph)` exposes `GET /describe`, `GET /snapshot`, `WS /observe` over a registered `Graph`. Keep these thin — all logic belongs in `core` / `extra` / `patterns`.
78
+
79
+ ### Design docs that back the above
80
+
81
+ - Phase 4+ factory authors must read `~/src/graphrefly/COMPOSITION-GUIDE.md` before composing primitives — covers lazy activation, subscription ordering, null guards, feedback cycles, `prompt_node` SENTINEL, wiring order.
82
+ - Backlog / anti-patterns / deferred follow-ups: `~/src/graphrefly-ts/docs/optimizations.md` (cross-language).
83
+ - Historical design decisions and parity notes: `~/src/graphrefly-ts/archive/optimizations/` (cross-language).
84
+
85
+ ## Generated docs workflow
86
+
87
+ Python API pages are generated from docstrings:
88
+
89
+ - Generator: `website/scripts/gen_api_docs.py`
90
+ - Output: `website/src/content/docs/api/*.md`
91
+ - Commands:
92
+ - `pnpm --dir website docs:gen`
93
+ - `pnpm --dir website docs:gen:check`
94
+ - `pnpm --dir website sync-docs`
95
+ - `pnpm --dir website sync-docs:check`
96
+
97
+ Do not hand-edit generated API pages.
98
+
99
+ ## Core design invariants
100
+
101
+ - No polling; propagation is reactive.
102
+ - No imperative triggers outside graph/message flow.
103
+ - No raw async primitives inside reactive layer logic.
104
+ - Use `src/graphrefly/core/clock.py` and `message_tier` helpers for timing/tier semantics.
105
+ - Keep Phase 4+ surfaces developer-oriented (hide protocol internals from primary API docs).
106
+ - Thread safety: per-subgraph `RLock`, per-node `_cache_lock`; no `async def` / `Awaitable` in public APIs.
107
+
108
+ ## Harness engineering coverage
109
+
110
+ GraphReFly covers the 8 requirements of a production agent harness — context/state control, execution boundary, control flow, verification, observability, policy/safety, human governance, continuous improvement. Mapping lives in `README.md` and `~/src/graphrefly-ts/archive/docs/SESSION-harness-engineering-strategy.md`.
111
+
112
+ ## Reactive collaboration loop (§9.0)
113
+
114
+ `harness_loop()` wires a static topology: INTAKE → TRIAGE → QUEUE → GATE → EXECUTE → VERIFY → REFLECT. Static topology + flowing data (the Kafka insight). Humans steer via `gate.modify(fn, n)`. Strategy model (`root_cause × intervention → success_rate`) feeds TRIAGE routing. Design source: `~/src/graphrefly-ts/archive/docs/SESSION-reactive-collaboration-harness.md`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "graphrefly"
3
- version = "0.19.0"
3
+ version = "0.20.0"
4
4
  description = "Reactive harness layer for agent workflows. Describe automations in plain language, trace every decision, enforce policies, persist checkpoints. Zero dependencies."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -158,13 +158,19 @@ def reduce( # noqa: A001 — roadmap API name
158
158
  def _op(src: Node[Any]) -> Node[Any]:
159
159
  acc = [seed]
160
160
  saw_data = [False]
161
+ dep_has_data = [False]
161
162
 
162
163
  def compute(deps: list[Any], _actions: NodeActions) -> Any:
164
+ if not dep_has_data[0]:
165
+ return None
163
166
  saw_data[0] = True
164
167
  acc[0] = reducer(acc[0], deps[0])
165
168
  return None
166
169
 
167
170
  def on_message(msg: Any, _index: int, actions: NodeActions) -> bool:
171
+ if msg[0] is MessageType.DATA:
172
+ dep_has_data[0] = True
173
+ return False
168
174
  if msg[0] is MessageType.COMPLETE:
169
175
  if not saw_data[0]:
170
176
  acc[0] = seed
@@ -329,10 +335,13 @@ def take_while(predicate: Callable[[Any], bool]) -> PipeOperator:
329
335
 
330
336
  def _op(src: Node[Any]) -> Node[Any]:
331
337
  done = [False]
338
+ dep_has_data = [False]
332
339
 
333
340
  def compute(deps: list[Any], actions: NodeActions) -> Any:
334
341
  if done[0]:
335
342
  return None
343
+ if not dep_has_data[0]:
344
+ return None
336
345
  v = deps[0]
337
346
  if not predicate(v):
338
347
  done[0] = True
@@ -340,9 +349,15 @@ def take_while(predicate: Callable[[Any], bool]) -> PipeOperator:
340
349
  return None
341
350
  return v
342
351
 
352
+ def on_message(msg: Any, _index: int, _actions: NodeActions) -> bool:
353
+ if msg[0] is MessageType.DATA:
354
+ dep_has_data[0] = True
355
+ return False
356
+
343
357
  return node(
344
358
  [src],
345
359
  compute,
360
+ on_message=on_message,
346
361
  describe_kind="take_while",
347
362
  complete_when_deps_complete=False,
348
363
  )
@@ -494,15 +509,14 @@ def last(*, default: Any = _LAST_NO_DEFAULT) -> PipeOperator:
494
509
  return True
495
510
  return False
496
511
 
497
- init = default if use_default else None
498
- return node(
499
- [src],
500
- compute,
501
- on_message=on_message,
502
- describe_kind="last",
503
- initial=init,
504
- complete_when_deps_complete=False,
505
- )
512
+ opts: dict[str, Any] = {
513
+ "on_message": on_message,
514
+ "describe_kind": "last",
515
+ "complete_when_deps_complete": False,
516
+ }
517
+ if use_default:
518
+ opts["initial"] = default
519
+ return node([src], compute, **opts)
506
520
 
507
521
  return _op
508
522
 
@@ -145,7 +145,6 @@ def switch_map(
145
145
  def _op(outer: Node[Any]) -> Node[Any]:
146
146
  inner_unsub: Callable[[], None] | None = None
147
147
  source_done = False
148
- attached = False
149
148
 
150
149
  def clear_inner() -> None:
151
150
  nonlocal inner_unsub
@@ -154,8 +153,7 @@ def switch_map(
154
153
  inner_unsub = None
155
154
 
156
155
  def attach(v: Any, a: NodeActions) -> None:
157
- nonlocal attached, inner_unsub
158
- attached = True
156
+ nonlocal inner_unsub
159
157
  clear_inner()
160
158
 
161
159
  def _on_inner_complete() -> None:
@@ -165,9 +163,7 @@ def switch_map(
165
163
 
166
164
  inner_unsub = _forward_inner(_as_node(fn(v)), a, _on_inner_complete)
167
165
 
168
- def compute(deps: list[Any], a: NodeActions) -> Any:
169
- if not attached:
170
- attach(deps[0], a)
166
+ def compute(_deps: list[Any], _a: NodeActions) -> Any:
171
167
  return clear_inner
172
168
 
173
169
  def on_message(msg: Any, _index: int, a: NodeActions) -> bool:
@@ -240,7 +236,6 @@ def concat_map(
240
236
  queue: deque[Any] = deque()
241
237
  inner_unsub: Callable[[], None] | None = None
242
238
  source_done = False
243
- attached = False
244
239
 
245
240
  def clear_inner() -> None:
246
241
  nonlocal inner_unsub
@@ -265,16 +260,12 @@ def concat_map(
265
260
  inner_unsub = _forward_inner(_as_node(fn(v)), a, _on_inner_complete)
266
261
 
267
262
  def enqueue(v: Any, a: NodeActions) -> None:
268
- nonlocal attached
269
- attached = True
270
263
  if max_buffer > 0 and len(queue) >= max_buffer:
271
264
  queue.popleft()
272
265
  queue.append(v)
273
266
  try_pump(a)
274
267
 
275
- def compute(deps: list[Any], a: NodeActions) -> Any:
276
- if not attached:
277
- enqueue(deps[0], a)
268
+ def compute(_deps: list[Any], _a: NodeActions) -> Any:
278
269
  return clear_inner
279
270
 
280
271
  def on_message(msg: Any, _index: int, a: NodeActions) -> bool:
@@ -355,7 +346,6 @@ def flat_map(
355
346
  source_done = False
356
347
  inner_stops: list[Callable[[], None]] = []
357
348
  buffer: deque[Any] = deque()
358
- attached = False
359
349
 
360
350
  def try_complete(a: NodeActions) -> None:
361
351
  if source_done and active == 0 and len(buffer) == 0:
@@ -397,11 +387,7 @@ def flat_map(
397
387
  active = 0
398
388
  buffer.clear()
399
389
 
400
- def compute(deps: list[Any], a: NodeActions) -> Any:
401
- nonlocal attached
402
- if not attached:
403
- attached = True
404
- enqueue(deps[0], a)
390
+ def compute(_deps: list[Any], _a: NodeActions) -> Any:
405
391
  return clear_all
406
392
 
407
393
  def on_message(msg: Any, _index: int, a: NodeActions) -> bool:
@@ -463,7 +449,6 @@ def exhaust_map(fn: Callable[[Any], Any], *, initial: Any = _UNSET) -> PipeOpera
463
449
  def _op(outer: Node[Any]) -> Node[Any]:
464
450
  inner_unsub: Callable[[], None] | None = None
465
451
  source_done = False
466
- attached = False
467
452
 
468
453
  def clear_inner() -> None:
469
454
  nonlocal inner_unsub
@@ -472,8 +457,7 @@ def exhaust_map(fn: Callable[[Any], Any], *, initial: Any = _UNSET) -> PipeOpera
472
457
  inner_unsub = None
473
458
 
474
459
  def attach(v: Any, a: NodeActions) -> None:
475
- nonlocal attached, inner_unsub
476
- attached = True
460
+ nonlocal inner_unsub
477
461
 
478
462
  def _on_inner_complete() -> None:
479
463
  clear_inner()
@@ -482,9 +466,7 @@ def exhaust_map(fn: Callable[[Any], Any], *, initial: Any = _UNSET) -> PipeOpera
482
466
 
483
467
  inner_unsub = _forward_inner(_as_node(fn(v)), a, _on_inner_complete)
484
468
 
485
- def compute(deps: list[Any], a: NodeActions) -> Any:
486
- if not attached and inner_unsub is None:
487
- attach(deps[0], a)
469
+ def compute(_deps: list[Any], _a: NodeActions) -> Any:
488
470
  return clear_inner
489
471
 
490
472
  def on_message(msg: Any, _index: int, a: NodeActions) -> bool:
@@ -11,10 +11,10 @@ import json
11
11
  import math
12
12
  import re as _re
13
13
  import threading
14
- from collections.abc import AsyncIterable, Iterable
14
+ from collections.abc import AsyncIterable, Callable, Iterable
15
15
  from dataclasses import dataclass, field
16
16
  from types import MappingProxyType
17
- from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable
17
+ from typing import TYPE_CHECKING, Any, Literal, Protocol, cast, runtime_checkable
18
18
 
19
19
  from graphrefly.core.cancellation import cancellation_token
20
20
  from graphrefly.core.clock import monotonic_ns
@@ -40,7 +40,7 @@ from graphrefly.patterns.messaging import TopicGraph, topic
40
40
  from graphrefly.patterns.orchestration import GateController, gate
41
41
 
42
42
  if TYPE_CHECKING:
43
- from collections.abc import Callable, Mapping, Sequence
43
+ from collections.abc import Mapping, Sequence
44
44
 
45
45
  from graphrefly.core.node import NodeImpl
46
46
 
@@ -831,6 +831,121 @@ def cost_meter_extractor(
831
831
  )
832
832
 
833
833
 
834
+ # ---------------------------------------------------------------------------
835
+ # Composition B: Content safety pipeline
836
+ # ---------------------------------------------------------------------------
837
+
838
+
839
+ def redactor(
840
+ stream_topic: TopicGraph,
841
+ patterns: list[Any],
842
+ replace_fn: Callable[[str], str] | None = None,
843
+ *,
844
+ name: str | None = None,
845
+ ) -> Node[StreamChunk]:
846
+ """Stream extractor that replaces matched patterns in the accumulated text.
847
+
848
+ Returns a derived node emitting a sanitized :class:`StreamChunk` on every
849
+ chunk: ``accumulated`` and ``token`` have matched substrings replaced by
850
+ ``replace_fn``. The default ``replace_fn`` replaces with ``"[REDACTED]"``.
851
+
852
+ Compose with :func:`content_gate` for in-flight safety pipelines.
853
+
854
+ Args:
855
+ stream_topic: Streaming topic to monitor.
856
+ patterns: List of compiled ``re.Pattern`` objects.
857
+ replace_fn: ``(match: str) -> str`` replacement function.
858
+ Defaults to ``lambda m: "[REDACTED]"``.
859
+ name: Optional node name.
860
+
861
+ Returns:
862
+ Derived node emitting sanitized :class:`StreamChunk` values.
863
+ """
864
+ _replace = replace_fn if replace_fn is not None else (lambda _m: "[REDACTED]")
865
+
866
+ def _sanitize(text: str) -> str:
867
+ result = text
868
+ for pat in patterns:
869
+ result = pat.sub(_replace, result)
870
+ return result
871
+
872
+ def _compute(dep_values: list[Any], _actions: Any) -> StreamChunk:
873
+ chunk = dep_values[0]
874
+ if chunk is None:
875
+ return StreamChunk(source="", token="", accumulated="", index=-1)
876
+ c: StreamChunk = chunk
877
+ return StreamChunk(
878
+ source=c.source,
879
+ token=_sanitize(c.token),
880
+ accumulated=_sanitize(c.accumulated),
881
+ index=c.index,
882
+ )
883
+
884
+ return derived([stream_topic.latest], _compute, name=name or "redactor")
885
+
886
+
887
+ # ---------------------------------------------------------------------------
888
+
889
+ ContentDecision = Literal["allow", "review", "block"]
890
+
891
+
892
+ def content_gate(
893
+ stream_topic: TopicGraph,
894
+ classifier: Callable[[str], float] | Node[float],
895
+ threshold: float,
896
+ *,
897
+ hard_multiplier: float = 1.5,
898
+ name: str | None = None,
899
+ ) -> Node[ContentDecision]:
900
+ """Derived node that classifies accumulated stream text.
901
+
902
+ Emits a three-way decision on every new chunk:
903
+
904
+ - ``"allow"`` — score below ``threshold``
905
+ - ``"review"`` — score in ``[threshold, threshold × hard_multiplier)``
906
+ - ``"block"`` — score at or above ``threshold × hard_multiplier``
907
+
908
+ Wire the output into a valve (automatic) or gate (human approval).
909
+ This node does not itself control flow — it classifies.
910
+
911
+ Args:
912
+ stream_topic: Streaming topic to classify.
913
+ classifier: ``(accumulated: str) -> float`` scoring function, or
914
+ a ``Node[float]`` for live scores.
915
+ threshold: Score at which output becomes ``"review"`` or
916
+ ``"block"``.
917
+ hard_multiplier: Multiplier on ``threshold`` for ``"block"`` boundary.
918
+ name: Optional node name.
919
+
920
+ Returns:
921
+ Derived node emitting :data:`ContentDecision`.
922
+ """
923
+ hard_threshold = threshold * hard_multiplier
924
+ classifier_fn: Callable[[str], float] | None = classifier if callable(classifier) else None
925
+ classifier_node: Node[float] | None = None if callable(classifier) else classifier
926
+
927
+ deps: list[Node[Any]] = [stream_topic.latest]
928
+ if classifier_node is not None:
929
+ deps.append(classifier_node)
930
+
931
+ def _compute(dep_values: list[Any], _actions: Any) -> ContentDecision:
932
+ chunk = dep_values[0]
933
+ if chunk is None:
934
+ return "allow"
935
+ score: float = (
936
+ dep_values[1] if (classifier_node is not None and len(dep_values) > 1) else 0.0
937
+ )
938
+ if classifier_fn is not None:
939
+ score = classifier_fn(chunk.accumulated)
940
+ if score >= hard_threshold:
941
+ return "block"
942
+ if score >= threshold:
943
+ return "review"
944
+ return "allow"
945
+
946
+ return derived(deps, _compute, name=name or "content-gate", initial="allow")
947
+
948
+
834
949
  # ---------------------------------------------------------------------------
835
950
  # gated_stream
836
951
  # ---------------------------------------------------------------------------