From 806cb31e5625eefef75f2ebb5843844d70966f8e Mon Sep 17 00:00:00 2001 From: LHMQ878 <72402929@cityu-dg.edu.cn> Date: Mon, 3 Aug 2026 23:53:15 +0800 Subject: [PATCH] fix(emitter): keep an unset group_id unset when cloning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Emitter.clone()` passed the group id positionally through `str()`, so the default `None` became the literal string `"None"`. `_create_event()` stamps `group_id=self._group_id` onto every `EventMeta` and `child()` falls back to it, so the fake value propagated to every event emitted by the clone and by every emitter descended from it: `is None` returned `False` and truthiness returned `True`. Agents are the common case here. All nine `clone()` implementations do `cloned.emitter = await self.emitter.clone()`, and agents build their emitter via `Emitter.root().child(namespace=[...], creator=self, events=...)` with no group id, so the broken path is the normal one. Pass the arguments by keyword and drop the coercion. `creator` no longer needs the `if self.creator else None` guard — it is already `object | None`. The existing `test_clone` constructs with `group_id="test_group"`, and a real group id round-trips through `str()` unchanged, which is why the default path was never covered. The TypeScript emitter does `this.groupId = input?.groupId` and preserves the absent value, so this was a Python-only divergence. Signed-off-by: LHMQ878 <72402929@cityu-dg.edu.cn> --- python/beeai_framework/emitter/emitter.py | 12 +++--- python/tests/test_emitter.py | 46 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/python/beeai_framework/emitter/emitter.py b/python/beeai_framework/emitter/emitter.py index 763fd5a39..6ddd80729 100644 --- a/python/beeai_framework/emitter/emitter.py +++ b/python/beeai_framework/emitter/emitter.py @@ -281,12 +281,12 @@ def _create_event(self, name: str) -> EventMeta: async def clone(self) -> "Emitter": cloned = Emitter( - str(self._group_id), - self.namespace.copy(), - self.creator if self.creator else None, - self.context.copy(), - self.trace.model_copy() if self.trace else None, - self._events.copy(), + group_id=self._group_id, + namespace=self.namespace.copy(), + creator=self.creator, + context=self.context.copy(), + trace=self.trace.model_copy() if self.trace else None, + events=self._events.copy(), ) for listener in self._listeners: cloned.on(listener.raw, listener.callback, listener.options.model_copy() if listener.options else None) diff --git a/python/tests/test_emitter.py b/python/tests/test_emitter.py index 6c5288121..9576b4c4c 100644 --- a/python/tests/test_emitter.py +++ b/python/tests/test_emitter.py @@ -64,6 +64,52 @@ async def test_clone() -> None: assert clone.events is not emitter.events +@pytest.mark.unit +@pytest.mark.asyncio +async def test_clone_preserves_group_id() -> None: + emitter = Emitter(group_id="test_group", namespace=["namespace"]) + clone = await emitter.clone() + + assert clone._group_id == "test_group" + + +@pytest.mark.unit +@pytest.mark.asyncio +async def test_clone_keeps_absent_group_id_absent() -> None: + # Agents build their emitter without a group id, so the clone must not turn + # the absent value into something truthy. + emitter = Emitter(namespace=["namespace"]) + assert emitter._group_id is None + + clone = await emitter.clone() + + assert clone._group_id is None + + +@pytest.mark.unit +@pytest.mark.asyncio +async def test_clone_emits_events_without_a_group_id() -> None: + emitter = Emitter(namespace=["app"]) + clone = await emitter.clone() + + group_ids: list[str | None] = [] + clone.on("*", lambda _, event: group_ids.append(event.group_id)) + await clone.emit("a", 1) + + assert group_ids == [None] + + +@pytest.mark.unit +@pytest.mark.asyncio +async def test_child_of_clone_inherits_absent_group_id() -> None: + emitter = Emitter(namespace=["app"]) + clone = await emitter.clone() + + child = clone.child(namespace=["child"]) + + assert child._group_id is None + + class TestEventsPropagation: @pytest.mark.unit @pytest.mark.asyncio