Skip to content

Commit c256383

Browse files
declan-scaleclaude
andcommitted
refactor(harness): inject streaming into UnifiedEmitter + cover auto_send_turn + doc tracer modes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a61f8c0 commit c256383

2 files changed

Lines changed: 73 additions & 5 deletions

File tree

src/agentex/lib/core/harness/emitter.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import AsyncIterator
5+
from typing import AsyncGenerator
66

77
from agentex.lib.core.harness.auto_send import auto_send
88
from agentex.lib.core.harness.tracer import SpanTracer
@@ -13,8 +13,13 @@
1313
class UnifiedEmitter:
1414
"""Ties trace context + chosen delivery together.
1515
16-
Tracing is default-on whenever `trace_id` is truthy; pass `tracer=False` to
17-
disable, or a custom `SpanTracer` to override.
16+
Tracing modes (the `tracer` arg):
17+
- tracer=None (default): auto-construct a SpanTracer if `trace_id` is present.
18+
- tracer=False: disable tracing entirely, regardless of `trace_id`.
19+
- tracer=<SpanTracer>: use the supplied instance.
20+
21+
`tracing` and `streaming` are injection escape-hatches for tests/advanced
22+
use; leave them None in production so the real adk modules are used.
1823
"""
1924

2025
tracer: SpanTracer | None
@@ -26,10 +31,12 @@ def __init__(
2631
parent_span_id: str | None,
2732
tracer: SpanTracer | bool | None = None,
2833
tracing: object | None = None,
34+
streaming: object | None = None,
2935
):
3036
self.task_id = task_id
3137
self.trace_id = trace_id
3238
self.parent_span_id = parent_span_id
39+
self._streaming = streaming
3340
if tracer is False:
3441
self.tracer = None
3542
elif isinstance(tracer, SpanTracer):
@@ -44,7 +51,7 @@ def __init__(
4451
else:
4552
self.tracer = None
4653

47-
async def yield_turn(self, turn: HarnessTurn) -> AsyncIterator[StreamTaskMessage]:
54+
async def yield_turn(self, turn: HarnessTurn) -> AsyncGenerator[StreamTaskMessage, None]:
4855
"""Sync HTTP ACP delivery: forward events, trace as side effect."""
4956
async for event in yield_events(turn.events, tracer=self.tracer):
5057
yield event
@@ -55,5 +62,6 @@ async def auto_send_turn(self, turn: HarnessTurn) -> TurnResult:
5562
turn.events,
5663
task_id=self.task_id,
5764
tracer=self.tracer,
65+
streaming=self._streaming,
5866
usage=turn.usage(),
5967
)

tests/lib/core/harness/test_emitter.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
from agentex.lib.core.harness.emitter import UnifiedEmitter
44
from agentex.lib.core.harness.types import TurnUsage
5-
from agentex.types.task_message_update import StreamTaskMessageStart, StreamTaskMessageDone
5+
from agentex.types.task_message import TaskMessage
6+
from agentex.types.task_message_delta import TextDelta
7+
from agentex.types.task_message_update import (
8+
StreamTaskMessageDelta,
9+
StreamTaskMessageDone,
10+
StreamTaskMessageStart,
11+
)
612
from agentex.types.text_content import TextContent
713

814

@@ -14,6 +20,42 @@ async def end_span(self, **kw):
1420
pass
1521

1622

23+
class _FakeCtx:
24+
"""Minimal StreamingTaskMessageContext fake (see test_auto_send.py)."""
25+
26+
def __init__(self, sink, content_type, initial_content):
27+
self.sink = sink
28+
self.content_type = content_type
29+
self.task_message = TaskMessage(id="msg-1", task_id="task1", content=initial_content)
30+
31+
async def __aenter__(self):
32+
self.sink.append(("open", self.content_type))
33+
return self
34+
35+
async def __aexit__(self, *a):
36+
await self.close()
37+
return False
38+
39+
async def close(self):
40+
self.sink.append(("close", self.content_type))
41+
42+
async def stream_update(self, update):
43+
self.sink.append(("update", update))
44+
return update
45+
46+
47+
class _FakeStreaming:
48+
def __init__(self):
49+
self.sink = []
50+
51+
def streaming_task_message_context(
52+
self, task_id, initial_content, streaming_mode="coalesced", created_at=None
53+
):
54+
ctype = getattr(initial_content, "type", None)
55+
self.sink.append(("ctx", ctype))
56+
return _FakeCtx(self.sink, ctype, initial_content)
57+
58+
1759
class _Turn:
1860
def __init__(self, events_list, usage):
1961
self._events_list = events_list
@@ -54,3 +96,21 @@ async def test_emitter_tracing_default_on_when_trace_id_present():
5496
async def test_emitter_tracing_overridable_off():
5597
emitter = UnifiedEmitter(task_id="t", trace_id="trace1", parent_span_id="p", tracer=False)
5698
assert emitter.tracer is None
99+
100+
101+
@pytest.mark.asyncio
102+
async def test_emitter_auto_send_turn_returns_usage():
103+
usage = TurnUsage(model="m", input_tokens=5)
104+
events = [
105+
StreamTaskMessageStart(type="start", index=0,
106+
content=TextContent(type="text", author="agent", content="")),
107+
StreamTaskMessageDelta(type="delta", index=0,
108+
delta=TextDelta(type="text", text_delta="Hello")),
109+
StreamTaskMessageDone(type="done", index=0),
110+
]
111+
turn = _Turn(events, usage)
112+
fake = _FakeStreaming()
113+
emitter = UnifiedEmitter(task_id="t", trace_id=None, parent_span_id=None, streaming=fake)
114+
result = await emitter.auto_send_turn(turn)
115+
assert result.usage == usage
116+
assert result.final_text == "Hello"

0 commit comments

Comments
 (0)