Move topic conditions to node publisher routes - #106
Conversation
Topics are now pure FIFO queues: the routing predicate that lived on topic.condition moves to the publishing node as a publisher route, declared with Node.builder().publish_to(topic, when=predicate). PublishRoute/PublisherRouter live in node_base.py and publish_events routes via node.publisher_router.select(...), so a topic never drops an event. Node serialization carries the router (with `when` as a pickle-free callable reference) instead of the topic-name list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The predicates in conditions.py (has_tool_call, has_text_response, always_true, ...) are route predicates now, used as the `when=` on a node's publisher route rather than a topic condition. Relocate the module from grafi/topics/ to grafi/nodes/ next to node_base's router, update all importers, and refresh the manifest callable refs to the new module path. Since this branch already breaks manifest compatibility (topic condition -> publisher route), folding the path change in here avoids a second serialization break later. Downgrade the human_tool_approval example to plain pause/resume: its approve/deny verdict routing was built on topic conditions on the human-reply topics, which is a consumer-router capability that arrives with the tool-hooks change. It now mirrors simple_hitl_assistant (content-routed publisher routes, one human-response channel). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR makes topics pure FIFO queues by removing topic-level condition filtering and moving routing to node-level publisher routes (publish_to(..., when=...)) implemented via PublishRoute/PublisherRouter. It updates node/workflow serialization to persist the router (and pickle-free when predicates) and adjusts tests + example manifests accordingly.
Changes:
- Introduces
PublishRoute+PublisherRouteringrafi/nodes/node_base.pyand updatespublish_eventsto route vianode.publisher_router.select(...). - Removes topic
conditionserialization/deserialization and updates topic behavior/tests to always enqueue. - Updates assistants/examples/manifests and unit/integration tests to use publisher-route predicates (
grafi.nodes.conditions) instead of topic conditions (grafi.topics.conditions).
Reviewed changes
Copilot reviewed 45 out of 45 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/workflow/test_workflow_run.py | Updates workflow tests to use publisher_router instead of publish_to. |
| tests/workflow/test_utils.py | Updates publish-events test to mock publisher_router.select and adjusts tracker assertions/comments. |
| tests/workflow/test_recovery.py | Migrates recovery workflow construction to publisher_router. |
| tests/workflow/test_invocation_isolation.py | Migrates invocation isolation workflow to publisher_router. |
| tests/workflow/test_fanout_quiescence.py | Migrates fan-out workflows to publisher_router routes. |
| tests/workflow/test_event_driven_workflow.py | Updates workflow builder/serialization tests to assert publisher_router structure. |
| tests/topics/test_topic_factory.py | Removes condition from topic-factory test inputs. |
| tests/topics/test_topic_base.py | Replaces topic-condition serialization tests with pure-queue behavior tests. |
| tests/topics/test_topic_base_cache_integration.py | Updates cache integration tests to assert all events enqueue and condition is absent. |
| tests/topics/test_output_topic.py | Removes condition-based publish tests; asserts pure-queue enqueue behavior. |
| tests/topics/test_input_topic.py | Updates deserialization tests to reflect condition-free topic manifests. |
| tests/nodes/test_publisher_router.py | Adds unit tests for publisher-router selection + (de)serialization. |
| tests/nodes/test_node.py | Updates node serialization tests to validate publisher_router.routes instead of publish_to. |
| tests/nodes/test_node_base.py | Updates node-base validation and builder tests for router-based publishing. |
| tests/integration/test_serialization_roundtrip.py | Moves predicate serialization tests from topic condition to route when. |
| tests/assistants/test_publisher_routing_integration.py | Adds end-to-end tests for content-based publisher routing and fan-out/drop behavior. |
| tests/assistants/test_assistant.py | Updates assistant deserialization tests to construct nodes with publisher_router. |
| tests/assistants/test_assistant_mock_llm.py | Replaces topic conditions with route predicates (publish_to(..., when=...)) in mock assistant workflows. |
| tests_integration/simple_stream_assistant/simple_stream_function_call_assistant.py | Switches from topic conditions to publisher-route predicates for routing. |
| tests_integration/react_assistant/ReActAssistant_manifest.json | Updates manifest nodes to publisher_router and removes topic condition. |
| tests_integration/react_assistant/react_assistant.py | Moves ReAct routing from topic conditions to publisher routes. |
| tests_integration/input_output_topics/mimo_llm_assistant.py | Refactors MIMO example to use a router node + publisher routes for content gating. |
| tests_integration/input_output_topics/mimo_llm_assistant_example.py | Updates expected event counts based on the new router node path. |
| tests_integration/hith_assistant/simple_hitl_assistant.py | Switches HITL example from topic conditions to publisher-route predicates. |
| tests_integration/hith_assistant/kyc_assistant.py | Switches KYC routing from topic conditions to publisher-route predicates. |
| tests_integration/hith_assistant/human_tool_approval_assistant.py | Downgrades to pause/resume and moves routing to publisher routes (per PR scope). |
| tests_integration/hith_assistant/human_tool_approval_assistant_example.py | Updates the HITL example to reflect pause/resume (no approve/deny branching). |
| tests_integration/function_call_assistant/SimpleFunctionCallAssistant_manifest.json | Updates manifest nodes to publisher_router and removes topic condition. |
| tests_integration/function_call_assistant/simple_openrouter_function_call_assistant.py | Moves function-call routing from topic conditions to publisher-route predicates. |
| tests_integration/function_call_assistant/simple_ollama_function_call_assistant.py | Moves function-call routing from topic conditions to publisher-route predicates. |
| tests_integration/function_call_assistant/simple_gemini_function_call_assistant.py | Moves function-call routing from topic conditions to publisher-route predicates. |
| tests_integration/function_call_assistant/simple_function_call_assistant.py | Moves function-call routing from topic conditions to publisher-route predicates. |
| tests_integration/function_call_assistant/simple_deepseek_function_call_assistant.py | Moves function-call routing from topic conditions to publisher-route predicates. |
| tests_integration/function_call_assistant/simple_claude_function_call_assistant.py | Moves function-call routing from topic conditions to publisher-route predicates. |
| tests_integration/function_call_assistant/multi_functions_call_assistant.py | Moves multi-tool routing from topic conditions to publisher-route predicates. |
| tests_integration/function_call_assistant/complex_function_manifest.json | Updates manifest nodes to publisher_router and removes topic condition. |
| tests_integration/function_assistant/SimpleFunctionLLMAssistant_manifest.json | Updates manifest nodes to publisher_router and removes topic condition. |
| grafi/workflows/impl/utils.py | Routes publications via node.publisher_router.select(...) instead of iterating node.publish_to. |
| grafi/topics/topic_impl/in_workflow_output_topic.py | Removes condition deserialization from in-workflow output topic. |
| grafi/topics/topic_factory.py | Updates topic-factory docs/examples to no longer mention condition. |
| grafi/topics/topic_base.py | Removes topic conditions, updates publish_data to always enqueue, and removes condition serialization. |
| grafi/nodes/node.py | Updates node deserialization to restore publisher_router instead of publish_to. |
| grafi/nodes/node_base.py | Adds PublishRoute/PublisherRouter and changes node serialization/publishing API accordingly. |
| grafi/nodes/conditions.py | Reframes conditions as reusable routing predicates and adds always_true. |
| grafi/agents/react_agent.py | Migrates ReAct agent routing from topic conditions to publisher-route predicates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| raw = data.get("when") | ||
| when = ( | ||
| deserialize_callable(raw, context=f"route '{data['topic']}' when") | ||
| if raw | ||
| else always_true | ||
| ) | ||
| return cls( | ||
| topic=topics[data["topic"]], | ||
| when=when, | ||
| ) |
There was a problem hiding this comment.
Done in 92eb9f8 — PublishRoute.from_dict now raises a ValueError naming the missing topic and listing the available ones (with a unit test in test_publisher_router.py).
| node_builder.publisher_router( | ||
| PublisherRouter.from_dict(node_dict["publisher_router"], topics) | ||
| ) |
There was a problem hiding this comment.
Done in 92eb9f8 — Node._builder_from_dict now raises a ValueError when publisher_router is absent, calling out the legacy publish_to key it replaced (covered by a new test in test_node.py).
| async def publish_data( | ||
| self, publish_event: PublishToTopicEvent | ||
| ) -> Optional[PublishToTopicEvent]: |
There was a problem hiding this comment.
Done in 92eb9f8 — publish_data now returns a non-optional PublishToTopicEvent and raises TopicPublicationError if the enqueue ever fails, so callers no longer handle an impossible None.
| if event: | ||
| published_events.append(event) | ||
| elif delivery_count: | ||
| # Condition not met: the event was never enqueued, so no consumer | ||
| # will ever commit these deliveries. Release them immediately. | ||
| # The topic dropped the event (still enqueued nothing), so no | ||
| # consumer will ever commit these deliveries. Release them now. |
There was a problem hiding this comment.
Done in 92eb9f8 — since publish_data never returns None now, the drop branch was dead code and has been removed (the misleading truthiness check and 'topic dropped' comment with it). Updated the test_utils case to assert both selected topics enqueue and nothing is discarded.
Copilot review on #106: - PublishRoute.from_dict: raise a descriptive ValueError naming the missing topic and the available ones, instead of a bare KeyError. - Node._builder_from_dict: raise a clear ValueError when a manifest lacks 'publisher_router', pointing at the legacy 'publish_to' it replaced, instead of a bare KeyError. - TopicBase.publish_data: return a non-optional PublishToTopicEvent and raise TopicPublicationError if enqueue ever fails, matching the "topics never drop" contract; drop the now-dead condition-drop branch in publish_events (and its truthiness check). Update the utils test to assert both selected topics enqueue and nothing is discarded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Makes topics pure FIFO queues and moves routing to the publishing node. The predicate that used to live on
topic.conditionis now a publisher route declared on the node:PublishRoute/PublisherRouterlive innode_base.py;publish_eventsroutes vianode.publisher_router.select(...), so a topic never drops an event. Node serialization carries the router (with eachwhenas a pickle-free callable reference) instead of thepublish_toname list, and topic serialization no longer emitscondition.The routing predicates also move:
grafi/topics/conditions.py→grafi/nodes/conditions.py(they're route predicates now, not topic conditions, and this keepstopics/independent ofnodes/). This branch is already the manifest-compatibility boundary — folding the module-path change in here avoids a second serialization break in the follow-up.Scope of two PRs
This is part 1 of 2. It contains only the conditions→routes move. The pre/post tool hooks +
HookedNode+ consumer router (HITL) land in a follow-up PR built on top of this one.One example is affected by the split:
human_tool_approval_assistant.pyimplemented approve/deny routing with topic conditions on the human-reply topics — a capability that returns via the consumer router in the hooks PR. Here it's downgraded to plain pause/resume, mirroringsimple_hitl_assistant(content-routed publisher routes, one human-response channel).Verification
ollama/_localskipped): hith 4/4, function_call 21/21 (incl. all providers + manifest deserialize round-trips), react 6/6, agents 2/2, input_output_topics, function_assistant, simple_stream, simple_llm, invoke_kwargs, multimodal, rag, embedding.🤖 Generated with Claude Code