Skip to content

fix(emitter): keep an unset group_id unset when cloning - #1582

Open
LHMQ878 wants to merge 1 commit into
i-am-bee:mainfrom
LHMQ878:fix/emitter-clone-group-id
Open

fix(emitter): keep an unset group_id unset when cloning#1582
LHMQ878 wants to merge 1 commit into
i-am-bee:mainfrom
LHMQ878:fix/emitter-clone-group-id

Conversation

@LHMQ878

@LHMQ878 LHMQ878 commented Aug 3, 2026

Copy link
Copy Markdown

Which issue(s) does this pull-request address?

Closes: #1581

Description

Emitter.clone() built the clone with the group id passed positionally through str():

cloned = Emitter(
    str(self._group_id),
    ...
)

group_id is str | None and defaults to None, so when it was unset the clone received the literal string "None". _create_event() stamps group_id=self._group_id onto every EventMeta, and child() does group_id=group_id or self._group_id, so the synthetic value propagated to every event emitted by the clone and by every emitter descended from it.

Measured on main (21284d7, Python 3.12.10):

expression before after
Emitter(namespace=["app"])._group_id None None
(await that.clone())._group_id 'None' None
cl._group_id is None False True
bool(cl._group_id) True False
event.group_id from the clone 'None' None
(await that.clone()).child(...)._group_id 'None' None
clone with group_id="run-42" 'run-42' 'run-42'

Agents are the common case rather than an edge case. All nine clone() implementations do cloned.emitter = await self.emitter.clone() — the four adapter agents (a2a, acp, agentstack, watsonx_orchestrate), the four core agents (lite, react, requirement, tool_calling), and context.py:281 — and agents build their emitter via Emitter.root().child(namespace=["agent", "tool_calling"], creator=self, events=...) with no group id. Events off a cloned agent looked like this:

run.agent.tool_calling.start     group_id='None'
agent.tool_calling.done          group_id='None'

Anything grouping or filtering by group_id therefore saw a spurious "None" group for cloned agents: if meta.group_id: became true, meta.group_id is None became false, and a dict keyed by group id gained a "None" bucket. Forwarding meta.group_id to a tracing backend recorded the string as-is.

The fix passes the constructor arguments by keyword and drops the coercion, so the absent value stays absent. creator no longer needs the if self.creator else None guard — the attribute is already object | None, so the conditional was a no-op.

The TypeScript emitter does this.groupId = input?.groupId (typescript/src/emitter/emitter.ts:60) and preserves the absent value through child() (:80) and createSnapshot() (:254), so this was a Python-only divergence and no TypeScript change is needed.

Why the existing test missed it

test_clone constructs with group_id="test_group", and a real group id round-trips through str() unchanged, so the only broken path — the default None — was never exercised. Four tests are added next to it:

test red before this change
test_clone_keeps_absent_group_id_absent AssertionError: assert 'None' is None
test_clone_emits_events_without_a_group_id assert ['None'] == [None]
test_child_of_clone_inherits_absent_group_id AssertionError: assert 'None' is None
test_clone_preserves_group_id green both ways — regression guard for the real-id path

Checklist

General

Code quality checks

  • Code quality checks pass: ruff check and ruff format --check are clean on both changed files. mypy beeai_framework/emitter/emitter.py reports only four pre-existing findings unrelated to this diff (missing deprecated/cachetools stubs, utils/schema.py:67, backend/chat.py:489) — identical with the change reverted.

Testing

  • Unit tests pass — python -m pytest -m unit, run on Windows/Python 3.12.10:

    before after
    passed 290 293
    failed 4 1
    skipped 4 4

    The 3 resolved failures are the new tests. The 1 remaining failure is the same in both runs and unrelated: tests/tools/filesystem/test_read_edit.py::test_tools_route_through_installed_backend fails with ToolInputValidationError: 'path' must be absolute, got: '/tmp/virtual.txt' — a POSIX path that is not absolute on Windows, so it is a platform artefact of my environment, not a regression.

    Two collection paths are excluded in my environment and in both runs equally: beeai_framework/adapters/transformers and tests/backend/providers/test_transformers.py, which fail to import with ModuleNotFoundError: No module named 'outlines_core.fsm' from the installed outlines package.

  • E2E tests pass: mise test:e2e — not run; they require model-provider credentials I do not have. This change touches no provider code path.

  • Tests are included (for bug fixes or new features)

Documentation

  • Documentation is updated — no doc change needed; the fix restores the documented str | None contract rather than altering it.
  • Embedme embeds code examples in docs — no examples touched.

`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>
@LHMQ878
LHMQ878 requested a review from a team as a code owner August 3, 2026 15:54
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Aug 3, 2026
@github-actions github-actions Bot added the python Python related functionality label Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Python related functionality size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emitter.clone() turns an unset group_id into the literal string "None" (Python)

1 participant