| # SPEC 17: Accumulator Pattern for Agent Events | |
| **Status**: IMPLEMENTED | |
| **Created**: 2025-12-02 | |
| **Author**: AI Agent | |
| **Related**: P0_REPR_BUG_ROOT_CAUSE_ANALYSIS.md | |
| ## 1. Context | |
| The Microsoft Agent Framework event model has a specific intended usage pattern: | |
| - `MagenticAgentDeltaEvent.text` β **Content Source** (Streaming) | |
| - `MagenticAgentMessageEvent` β **Completion Signal** (End of Turn) | |
| Our previous implementation incorrectly attempted to extract content from `MagenticAgentMessageEvent.message`. This property is not designed for content extraction and can contain internal representation data (repr strings) for tool-only messages. This led to the "repr bug" where users saw raw Python object strings in the UI. | |
| The **Accumulator Pattern** aligns our codebase with Microsoft's intended architecture (as demonstrated in their `04_magentic_one.py` sample) and resolves the display issues by using the correct event data source. | |
| ## 2. The Solution: Accumulator Pattern | |
| Instead of relying on the final message event for content, we adopt the **Accumulator Pattern**, which aligns with the Microsoft Agent Framework's intended usage (as seen in their sample `04_magentic_one.py`). | |
| ### 2.1 Core Concept | |
| 1. **Streaming is Truth**: `MagenticAgentDeltaEvent` is the exclusive source of text content. These events are not affected by the upstream bug. | |
| 2. **Accumulation**: The orchestrator maintains a stateful buffer (`current_message_buffer`) that appends text from delta events. | |
| 3. **Signal Processing**: `MagenticAgentMessageEvent` is treated solely as a completion signal ("end of turn"). When received, we consume the buffer to form the final UI message and then clear the buffer. | |
| ### 2.2 Logic Flow | |
| ```python | |
| current_message_buffer = "" | |
| for event in stream: | |
| if event is DeltaEvent: | |
| current_message_buffer += event.text | |
| emit_streaming_event(event.text) | |
| elif event is MessageEvent: | |
| # IGNORE event.message (it might be corrupted) | |
| final_text = current_message_buffer | |
| if not final_text: | |
| final_text = "Action completed (Tool Call)" | |
| emit_complete_event(final_text) | |
| current_message_buffer = "" | |
| ``` | |
| ## 3. Test Plan | |
| To verify this pattern ensures correct output regardless of upstream bugs, we define the following test scenarios: | |
| ### 3.1 Scenario A: Standard Text Message | |
| - **Input**: Sequence of `MagenticAgentDeltaEvent` (with text parts) -> `MagenticAgentMessageEvent` (with corrupted repr). | |
| - **Expected Output**: The `AgentEvent` emitted at the end must contain the concatenated text from the deltas, NOT the repr string. | |
| ### 3.2 Scenario B: Tool Call (No Text) | |
| - **Input**: No text deltas -> `MagenticAgentMessageEvent` (with corrupted repr). | |
| - **Expected Output**: The `AgentEvent` should contain a fallback message (e.g., "Action completed (Tool Call)"), NOT the repr string. | |
| ## 4. Implementation Details | |
| The pattern is implemented in `src/orchestrators/advanced.py` within the `run()` method loop. It bypasses `_process_event` for these specific event types to ensure strict control over data flow. | |