Skip to content

Move topic conditions to node publisher routes - #106

Merged
GuanyiLi-Craig merged 3 commits into
mainfrom
gl/topic-conditions-to-node
Jul 2, 2026
Merged

Move topic conditions to node publisher routes#106
GuanyiLi-Craig merged 3 commits into
mainfrom
gl/topic-conditions-to-node

Conversation

@GuanyiLi-Craig

Copy link
Copy Markdown
Contributor

What

Makes topics pure FIFO queues and moves routing to the publishing node. The predicate that used to live on topic.condition is now a publisher route declared on the node:

# before
Topic(name="function_call", condition=has_tool_call)
Node.builder().publish_to(function_call).publish_to(agent_output)

# after
Node.builder()
    .publish_to(function_call, when=has_tool_call)
    .publish_to(agent_output, when=has_text_response)

PublishRoute / PublisherRouter live in node_base.py; publish_events routes via node.publisher_router.select(...), so a topic never drops an event. Node serialization carries the router (with each when as a pickle-free callable reference) instead of the publish_to name list, and topic serialization no longer emits condition.

The routing predicates also move: grafi/topics/conditions.pygrafi/nodes/conditions.py (they're route predicates now, not topic conditions, and this keeps topics/ independent of nodes/). 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.py implemented 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, mirroring simple_hitl_assistant (content-routed publisher routes, one human-response channel).

Verification

  • 705 unit tests pass; mypy clean; pre-commit (isort/black/flake8) clean.
  • Full integration suite green against real APIs (53 example scripts, 0 failures; only ollama/_local skipped): 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

GuanyiLi-Craig and others added 2 commits July 2, 2026 13:41
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>
Copilot AI review requested due to automatic review settings July 2, 2026 18:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + PublisherRouter in grafi/nodes/node_base.py and updates publish_events to route via node.publisher_router.select(...).
  • Removes topic condition serialization/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.

Comment thread grafi/nodes/node_base.py
Comment on lines +58 to +67
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,
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 92eb9f8PublishRoute.from_dict now raises a ValueError naming the missing topic and listing the available ones (with a unit test in test_publisher_router.py).

Comment thread grafi/nodes/node.py
Comment on lines +111 to +113
node_builder.publisher_router(
PublisherRouter.from_dict(node_dict["publisher_router"], topics)
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 92eb9f8Node._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).

Comment thread grafi/topics/topic_base.py Outdated
Comment on lines 40 to 42
async def publish_data(
self, publish_event: PublishToTopicEvent
) -> Optional[PublishToTopicEvent]:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 92eb9f8publish_data now returns a non-optional PublishToTopicEvent and raises TopicPublicationError if the enqueue ever fails, so callers no longer handle an impossible None.

Comment thread grafi/workflows/impl/utils.py Outdated
Comment on lines +120 to +124
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@GuanyiLi-Craig
GuanyiLi-Craig merged commit fc53b22 into main Jul 2, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants