Skip to content

Commit af1fab4

Browse files
praisonai-triage-agent[bot]MervinPraisonCopilot
authored
feat: emit ContextTraceEmitter events in managed agents (fixes #1427) (#1433)
* feat: emit ContextTraceEmitter events in managed agents (fixes #1427) - Add trace event emission to AnthropicManagedAgent._execute_sync - Emit agent_start before SSE stream - Emit tool_call_start/end around agent.tool_use events - Emit llm_response when aggregated text is available - Emit agent_end on session completion - Add trace event emission to LocalManagedAgent._execute_sync - Emit agent_start/end around agent.chat() calls - Emit llm_response for response content - Add comprehensive unit tests for trace event functionality - Zero-overhead when no emitter is installed (get_context_emitter() returns disabled singleton) - Enables non-empty HTML traces for langextract/langfuse integration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com> * test: fix managed trace test imports and mock usage fields Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/cb576383-1eb7-4876-8227-b35a8bc2cc6f Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com> * fix: coerce usage tokens to int to handle non-numeric SDK stubs --------- Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
1 parent 0805e02 commit af1fab4

File tree

3 files changed

+374
-45
lines changed

3 files changed

+374
-45
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
"""
2+
Tests for managed agent trace events emission.
3+
4+
Verifies that AnthropicManagedAgent and LocalManagedAgent emit proper
5+
ContextTraceEmitter events so that langextract/langfuse traces are non-empty.
6+
"""
7+
8+
import pytest
9+
from unittest.mock import Mock, patch
10+
from praisonaiagents.trace.context_events import (
11+
ContextListSink,
12+
ContextTraceEmitter,
13+
ContextEventType,
14+
trace_context
15+
)
16+
17+
18+
class TestAnthropicManagedAgentTraceEvents:
19+
"""Test trace event emission for AnthropicManagedAgent."""
20+
21+
def test_execute_sync_emits_trace_events(self):
22+
"""Test that _execute_sync emits agent_start, llm_response, and agent_end events."""
23+
from praisonai.integrations.managed_agents import AnthropicManagedAgent, ManagedConfig
24+
25+
# Create a mock client and session
26+
mock_client = Mock()
27+
mock_stream = Mock()
28+
mock_stream.__enter__ = Mock(return_value=mock_stream)
29+
mock_stream.__exit__ = Mock(return_value=None)
30+
31+
# Mock events for the stream
32+
mock_event = Mock()
33+
mock_event.type = "session.status_idle"
34+
mock_stream.__iter__ = Mock(return_value=iter([mock_event]))
35+
36+
mock_client.beta.sessions.events.stream.return_value = mock_stream
37+
38+
# Create agent with mocked client
39+
config = ManagedConfig(name="TestAgent", system="Test system")
40+
agent = AnthropicManagedAgent(config=config)
41+
agent._client = mock_client
42+
agent.agent_id = "test_agent_id"
43+
agent.environment_id = "test_env_id"
44+
agent._session_id = "test_session_id"
45+
46+
# Set up trace sink
47+
sink = ContextListSink()
48+
emitter = ContextTraceEmitter(sink=sink, session_id="test_session", enabled=True)
49+
50+
with trace_context(emitter):
51+
agent._execute_sync("Write a haiku")
52+
53+
# Verify events were emitted
54+
events = sink.get_events()
55+
assert len(events) >= 2, f"Expected at least 2 events, got {len(events)}"
56+
57+
# Check agent_start event
58+
start_events = [e for e in events if e.event_type == ContextEventType.AGENT_START]
59+
assert len(start_events) == 1, f"Expected 1 agent_start event, got {len(start_events)}"
60+
assert start_events[0].agent_name == "TestAgent"
61+
assert start_events[0].data["input"] == "Write a haiku"
62+
assert start_events[0].data["goal"] == "Test system"
63+
64+
# Check agent_end event
65+
end_events = [e for e in events if e.event_type == ContextEventType.AGENT_END]
66+
assert len(end_events) == 1, f"Expected 1 agent_end event, got {len(end_events)}"
67+
assert end_events[0].agent_name == "TestAgent"
68+
69+
def test_process_events_emits_tool_events(self):
70+
"""Test that _process_events emits tool_call_start and tool_call_end for tool_use events."""
71+
from praisonai.integrations.managed_agents import AnthropicManagedAgent, ManagedConfig
72+
73+
# Create agent
74+
config = ManagedConfig(name="TestAgent")
75+
agent = AnthropicManagedAgent(config=config)
76+
77+
# Mock tool_use event
78+
mock_event = Mock()
79+
mock_event.type = "agent.tool_use"
80+
mock_event.name = "test_tool"
81+
mock_event.id = "tool_123"
82+
mock_event.input = {"query": "test"}
83+
mock_event.needs_confirmation = False
84+
mock_event.usage = None
85+
mock_event.model_usage = None
86+
87+
# Mock session idle event
88+
mock_idle = Mock()
89+
mock_idle.type = "session.status_idle"
90+
mock_idle.usage = None
91+
mock_idle.model_usage = None
92+
93+
# Set up trace sink
94+
sink = ContextListSink()
95+
emitter = ContextTraceEmitter(sink=sink, session_id="test_session", enabled=True)
96+
97+
# Call _process_events with emitter
98+
with trace_context(emitter):
99+
text_parts, tool_log = agent._process_events(
100+
client=Mock(),
101+
session_id="test_session",
102+
stream=[mock_event, mock_idle],
103+
emitter=emitter
104+
)
105+
106+
# Verify tool events were emitted
107+
events = sink.get_events()
108+
109+
start_events = [e for e in events if e.event_type == ContextEventType.TOOL_CALL_START]
110+
assert len(start_events) == 1, f"Expected 1 tool_call_start event, got {len(start_events)}"
111+
assert start_events[0].agent_name == "TestAgent"
112+
assert start_events[0].data["tool_name"] == "test_tool"
113+
assert start_events[0].data["tool_args"] == {"query": "test"}
114+
115+
end_events = [e for e in events if e.event_type == ContextEventType.TOOL_CALL_END]
116+
assert len(end_events) == 1, f"Expected 1 tool_call_end event, got {len(end_events)}"
117+
assert end_events[0].agent_name == "TestAgent"
118+
assert end_events[0].data["tool_name"] == "test_tool"
119+
assert end_events[0].data["duration_ms"] >= 0
120+
121+
122+
class TestLocalManagedAgentTraceEvents:
123+
"""Test trace event emission for LocalManagedAgent."""
124+
125+
def test_execute_sync_emits_trace_events(self):
126+
"""Test that _execute_sync emits agent_start, llm_response, and agent_end events."""
127+
from praisonai.integrations.managed_local import LocalManagedAgent, LocalManagedConfig
128+
129+
# Create agent with minimal config
130+
config = LocalManagedConfig(name="TestAgent", system="Test system", tools=[])
131+
agent = LocalManagedAgent(config=config)
132+
133+
# Mock the inner agent
134+
mock_inner_agent = Mock()
135+
mock_inner_agent.chat.return_value = "This is a haiku response"
136+
agent._inner_agent = mock_inner_agent
137+
agent.agent_id = "test_agent_id"
138+
agent.environment_id = "test_env_id"
139+
agent._session_id = "test_session_id"
140+
141+
# Mock session store methods
142+
agent._persist_message = Mock()
143+
agent._sync_usage = Mock()
144+
agent._persist_state = Mock()
145+
146+
# Set up trace sink
147+
sink = ContextListSink()
148+
emitter = ContextTraceEmitter(sink=sink, session_id="test_session", enabled=True)
149+
150+
with trace_context(emitter):
151+
result = agent._execute_sync("Write a haiku")
152+
153+
assert result == "This is a haiku response"
154+
155+
# Verify events were emitted
156+
events = sink.get_events()
157+
assert len(events) >= 2, f"Expected at least 2 events, got {len(events)}"
158+
159+
# Check agent_start event
160+
start_events = [e for e in events if e.event_type == ContextEventType.AGENT_START]
161+
assert len(start_events) == 1, f"Expected 1 agent_start event, got {len(start_events)}"
162+
assert start_events[0].agent_name == "TestAgent"
163+
assert start_events[0].data["input"] == "Write a haiku"
164+
assert start_events[0].data["goal"] == "Test system"
165+
166+
# Check llm_response event
167+
response_events = [e for e in events if e.event_type == ContextEventType.LLM_RESPONSE]
168+
assert len(response_events) == 1, f"Expected 1 llm_response event, got {len(response_events)}"
169+
assert response_events[0].agent_name == "TestAgent"
170+
assert response_events[0].data["response_content"] == "This is a haiku response"
171+
172+
# Check agent_end event
173+
end_events = [e for e in events if e.event_type == ContextEventType.AGENT_END]
174+
assert len(end_events) == 1, f"Expected 1 agent_end event, got {len(end_events)}"
175+
assert end_events[0].agent_name == "TestAgent"
176+
177+
def test_zero_overhead_when_no_emitter(self):
178+
"""Test that trace events have zero overhead when no emitter is installed."""
179+
from praisonai.integrations.managed_local import LocalManagedAgent, LocalManagedConfig
180+
181+
# Create agent
182+
config = LocalManagedConfig(name="TestAgent", tools=[])
183+
agent = LocalManagedAgent(config=config)
184+
185+
# Mock the inner agent
186+
mock_inner_agent = Mock()
187+
mock_inner_agent.chat.return_value = "Response"
188+
agent._inner_agent = mock_inner_agent
189+
190+
# Mock session methods
191+
agent._persist_message = Mock()
192+
agent._sync_usage = Mock()
193+
agent._persist_state = Mock()
194+
195+
# Execute without any trace context - should work normally
196+
result = agent._execute_sync("Test prompt")
197+
198+
assert result == "Response"
199+
mock_inner_agent.chat.assert_called_once_with("Test prompt")
200+
201+
202+
class TestRealAgenticTest:
203+
"""Real agentic test with actual Agent and managed backend."""
204+
205+
@pytest.mark.skipif(True, reason="Gated real agentic test - requires API keys")
206+
def test_agent_with_managed_backend_shows_events(self):
207+
"""Real agentic test: Agent(backend=ManagedAgent()).start() with ContextListSink shows ≥ 2 events."""
208+
from praisonai.integrations.managed_local import LocalManagedAgent, LocalManagedConfig
209+
from praisonaiagents import Agent
210+
211+
# Create local managed backend
212+
managed_config = LocalManagedConfig(
213+
name="TestAgent",
214+
system="You are a helpful assistant. Respond in exactly one sentence.",
215+
tools=[], # No tools for simple test
216+
)
217+
managed_backend = LocalManagedAgent(config=managed_config)
218+
219+
# Create Agent with managed backend
220+
agent = Agent(name="test", backend=managed_backend)
221+
222+
# Set up trace collection
223+
sink = ContextListSink()
224+
emitter = ContextTraceEmitter(sink=sink, session_id="real_test", enabled=True)
225+
226+
# Run agent with trace context
227+
with trace_context(emitter):
228+
result = agent.start("Say hi")
229+
230+
print(f"Agent response: {result}")
231+
232+
# Verify we got events
233+
events = sink.get_events()
234+
print(f"Collected {len(events)} events:")
235+
for i, event in enumerate(events):
236+
print(f" {i+1}. {event.event_type} - {event.agent_name}")
237+
238+
assert len(events) >= 2, f"Expected ≥ 2 events for real agentic test, got {len(events)}"
239+
240+
# Should have at least agent_start and agent_end
241+
event_types = [e.event_type for e in events]
242+
assert ContextEventType.AGENT_START in event_types
243+
assert ContextEventType.AGENT_END in event_types

0 commit comments

Comments
 (0)