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.
- {graphrefly-0.19.0 → graphrefly-0.20.0}/CHANGELOG.md +16 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/CLAUDE.md +13 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/PKG-INFO +21 -4
- {graphrefly-0.19.0 → graphrefly-0.20.0}/README.md +20 -3
- graphrefly-0.20.0/llms.txt +114 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/pyproject.toml +1 -1
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/tier1.py +23 -9
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/tier2.py +6 -24
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/ai.py +118 -3
- graphrefly-0.20.0/src/graphrefly/patterns/harness/bridge.py +473 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_tier1.py +45 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_tier2.py +94 -22
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_harness.py +387 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/astro.config.mjs +4 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/Header.astro +1 -0
- graphrefly-0.19.0/llms.txt +0 -272
- graphrefly-0.19.0/src/graphrefly/patterns/harness/bridge.py +0 -161
- {graphrefly-0.19.0 → graphrefly-0.20.0}/.github/workflows/pages.yml +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/.github/workflows/release.yml +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/.gitignore +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/.mise.toml +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/CONTRIBUTING.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/GEMINI.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/LICENSE +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/benchmarks/py-baseline.json +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/docs/ADAPTER-CONTRACT.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/docs/benchmark.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/examples/README.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/examples/basic_counter.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/compat/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/compat/async_utils.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/compat/asyncio_runner.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/compat/trio_runner.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/bridge.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/cancellation.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/clock.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/dynamic_node.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/guard.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/meta.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/node.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/node_base.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/protocol.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/runner.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/subgraph_locks.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/sugar.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/timer.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/core/versioning.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/adapters.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/backoff.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/backpressure.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/cascading_cache.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/checkpoint.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/composite.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/cron.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/data_structures.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/resilience.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/extra/sources.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/codec.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/graph.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/profile.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/graph/sizeof.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/integrations/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/integrations/django.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/integrations/fastapi.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/_internal.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/cqrs.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/domain_templates.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/graphspec.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/loop.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/profile.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/strategy.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/trace.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/harness/types.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/memory.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/messaging.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/orchestration.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reactive_layout/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reactive_layout/measurement_adapters.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reactive_layout/reactive_block_layout.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reactive_layout/reactive_layout.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/patterns/reduction.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/src/graphrefly/py.typed +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/bench_core.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/conftest.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/helpers/__init__.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/helpers/mock_llm.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_adapter_contract.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_adapters_ingest.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_adapters_storage.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_backpressure.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_bridge.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_cascading_cache.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_concurrency.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_core.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_django.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_domain_templates.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_dynamic_node.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_edge_cases.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_composite.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_data_structures.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_resilience.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_sources.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_extra_sources_http.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_fastapi.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_graph.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_graphspec.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_guard.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_measurement_adapters.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_operator_protocol_matrix.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_ai.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_cqrs.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_memory.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_messaging.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_orchestration.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_patterns_reduction.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_perf_smoke.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_protocol.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_reactive_block_layout.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_reactive_layout.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_reduction.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_regressions.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_runner.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_smoke.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_sugar.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/tests/test_versioning.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/.gitignore +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/README.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/content.config.ts +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/package.json +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/pnpm-lock.yaml +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/public/llms.txt +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/py-api-sidebar.mjs +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/scripts/gen_api_docs.py +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/scripts/sync-docs.mjs +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/GraphreflyHero.astro +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/MobileMenuFooter.astro +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/PyodidePlayground.tsx +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/Sidebar.astro +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/components/SiteTitle.astro +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/BackoffPreset.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/BackoffStrategy.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/CheckpointAdapter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/CircuitBreaker.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/CircuitOpenError.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/CompactEntry.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/DeferWhen.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/DictCheckpointAdapter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/DistillBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/DownStrategy.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/Extraction.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/FileCheckpointAdapter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/HttpBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/JitterMode.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/MemoryCheckpointAdapter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/Message.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/MessageType.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/Messages.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/NodeActions.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/NodeFn.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/NodeImpl.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/NodeStatus.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/PipeOperator.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/PubSubHub.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveCounterBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveIndexBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveListBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveLogBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/ReactiveMapBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/SqliteCheckpointAdapter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/SubscribeHints.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/TimeoutError.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/TokenBucket.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/VerifiableBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/Versioned.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/WithBreakerBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/WithStatusBundle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/audit.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/batch.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/buffer.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/buffer_count.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/buffer_time.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/cache.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/cached.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/checkpoint_node_value.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/circuit_breaker.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/combine.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/concat.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/concat_map.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/constant.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/debounce.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/decorrelated_jitter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/delay.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/derived.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/dispatch_messages.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/distill.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/distinct_until_changed.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/down_with_batch.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/effect.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/element_at.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/empty.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/exhaust_map.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/exponential.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/fallback.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/fibonacci.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/filter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/find.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/first.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/first_value_from.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/flat_map.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/for_each.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_any.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_async_iter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_awaitable.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_cron.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_event_emitter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_fs_watch.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_git_hook.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_http.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_iter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_mcp.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_timer.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_webhook.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/from_websocket.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/gate.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/index.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/interval.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/is_batching.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/is_local_only.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/is_phase2_message.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/is_terminal_message.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/keepalive.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/last.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/linear.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/log_slice.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/map.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/merge.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/message_tier.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/never.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/node.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/of.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/operator.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/pairwise.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/partition_for_batch.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/pausable.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/pipe.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/producer.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/propagates_to_meta.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/pubsub.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/race.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/rate_limiter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_counter.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_index.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_list.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_log.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reactive_map.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/reduce.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/repeat.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/replay.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/rescue.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/resolve_backoff_preset.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/restore_graph_checkpoint.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/retry.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/sample.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/save_graph_checkpoint.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/scan.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/share.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/skip.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/start_with.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/state.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/subscribe.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/switch_map.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/take.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/take_until.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/take_while.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/tap.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/throttle.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/throw_error.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/timeout.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/timeout_node.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/to_array.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/to_list.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/to_sse.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/to_websocket.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/token_bucket.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/token_tracker.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/valve.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/verifiable.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/window.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/window_count.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/window_time.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/with_breaker.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/with_latest_from.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/with_max_attempts.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/with_status.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/api/zip.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/index.mdx +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/lab/python.mdx +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content/docs/recipes/index.md +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/content.config.ts +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/env.d.ts +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/src/styles/custom.css +0 -0
- {graphrefly-0.19.0 → graphrefly-0.20.0}/website/theme-prototypes.html +0 -0
- {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.
|
|
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
|
|
28
|
+
**The reactive harness layer for agent workflows.** Describe in plain language, review visually, run persistently, trace every decision.
|
|
29
29
|
|
|
30
|
-
|
|
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
|
[](https://pypi.org/project/graphrefly/)
|
|
33
33
|
[](./LICENSE)
|
|
34
34
|
[](https://pypi.org/project/graphrefly/)
|
|
35
35
|
|
|
36
|
-
[Docs](https://
|
|
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
|
|
3
|
+
**The reactive harness layer for agent workflows.** Describe in plain language, review visually, run persistently, trace every decision.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
[](https://pypi.org/project/graphrefly/)
|
|
8
8
|
[](./LICENSE)
|
|
9
9
|
[](https://pypi.org/project/graphrefly/)
|
|
10
10
|
|
|
11
|
-
[Docs](https://
|
|
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.
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
initial=
|
|
504
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
# ---------------------------------------------------------------------------
|