Skip to content

Commit 86da19c

Browse files
declan-scaleclaude
andcommitted
refactor(harness)!: consolidate the LangGraph harness, remove tracing handler
Collapse _langgraph_async / _langgraph_messages / _langgraph_tracing into _langgraph_sync (emit_langgraph_messages, convert helpers) and _langgraph_turn (stream_langgraph_events). Span tracing is now derived from the canonical stream by UnifiedEmitter, so create_langgraph_tracing_handler is removed. Public facade names are unchanged except the removed handler. All in-repo consumers were migrated to the unified surface in the preceding PRs. BREAKING CHANGE: create_langgraph_tracing_handler is removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fb4111f commit 86da19c

16 files changed

Lines changed: 344 additions & 747 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### ⚠ BREAKING CHANGES
6+
7+
* **harness:** removed the deprecated bespoke tracing handlers `create_langgraph_tracing_handler` / `create_pydantic_ai_tracing_handler` (and their `AgentexLangGraphTracingHandler` / `AgentexPydanticAITracingHandler` classes) from the public `agentex.lib.adk` surface. Span tracing is now derived from the canonical `StreamTaskMessage*` stream by `UnifiedEmitter` — wrap your run in the harness `*Turn` and drive `UnifiedEmitter.yield_turn` / `auto_send_turn`. The `agentex init` templates were migrated accordingly.
8+
* **harness:** each harness now exposes exactly `_<harness>_sync.py` + `_<harness>_turn.py` under `agentex.lib.adk._modules`. The OpenAI harness `OpenAITurn` and `convert_openai_to_agentex_events` moved to `agentex.lib.adk._modules._openai_turn` / `_openai_sync`; back-compat shims remain at `agentex.lib.adk.providers._modules.{openai_turn,sync_provider}` for one release. Public facade names (`stream_pydantic_ai_events`, `stream_langgraph_events`, `emit_langgraph_messages`, etc.) are unchanged.
9+
510
### Features
611

712
* **tracing:** emit OTel metrics for async span queue depth, batch drain, and SGP export success/failure (HTTP status labels). Disable SDK-side recording with ``AGENTEX_TRACING_METRICS=0``.

adk/docs/harness.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,17 @@ Every harness tap produces a sequence of these. Everything downstream (delivery,
3939

4040
## Per-harness taps: `convert_<harness>_to_agentex_events`
4141

42-
A tap is an async generator that translates the harness's native event stream into `StreamTaskMessage*` events. The currently shipped taps are:
42+
A tap is an async generator that translates the harness's native event stream into `StreamTaskMessage*` events. The shipped taps are:
4343

4444
| Harness | Tap function | Exported from |
4545
|---|---|---|
4646
| pydantic-ai | `convert_pydantic_ai_to_agentex_events` | `agentex.lib.adk` |
4747
| LangGraph | `convert_langgraph_to_agentex_events` | `agentex.lib.adk` |
48+
| claude-code | `convert_claude_code_to_agentex_events` | `agentex.lib.adk` |
49+
| codex | `convert_codex_to_agentex_events` | `agentex.lib.adk` |
50+
| OpenAI Agents | `convert_openai_to_agentex_events` | `agentex.lib.adk.providers._modules.sync_provider` |
4851

49-
Taps for claude-code and codex will be added in subsequent PRs (AGX1-420, AGX1-421) and exported from `agentex.lib.adk` in the same way.
52+
Each harness also provides a `HarnessTurn` wrapper that pairs its tap's event stream with usage extraction: `PydanticAITurn`, `LangGraphTurn`, `ClaudeCodeTurn`, `CodexTurn`, and `OpenAITurn`.
5053

5154
---
5255

@@ -157,11 +160,13 @@ Spans are derived from the canonical stream by `SpanDeriver` (pure, no `adk` dep
157160

158161
## Usage examples by channel
159162

160-
### Sync ACP (pydantic-ai tap)
163+
### Sync ACP (`yield_turn`)
164+
165+
Build the harness's `HarnessTurn` wrapper and iterate `emitter.yield_turn(turn)` — the emitter forwards each event to the caller and traces spans as a side effect:
161166

162167
```python
163168
import agentex.lib.adk as adk
164-
from agentex.lib.adk import UnifiedEmitter, convert_pydantic_ai_to_agentex_events
169+
from agentex.lib.adk import UnifiedEmitter, ClaudeCodeTurn
165170

166171
@acp.on_message_send
167172
async def handle(params):
@@ -172,13 +177,12 @@ async def handle(params):
172177
trace_id=task_id,
173178
parent_span_id=turn_span.id if turn_span else None,
174179
)
175-
tap = convert_pydantic_ai_to_agentex_events(pydantic_stream)
176-
# wrap tap in a HarnessTurn then yield_turn, or yield directly:
177-
async for event in tap:
180+
turn = ClaudeCodeTurn(claude_code_stream) # any HarnessTurn
181+
async for event in emitter.yield_turn(turn):
178182
yield event
179183
```
180184

181-
For the pre-unified sync path the tap is still yielded directly; `UnifiedEmitter.yield_turn` is the forward-looking integration point when a `HarnessTurn` wrapper is available.
185+
Every harness follows the same shape — swap `ClaudeCodeTurn` for `PydanticAITurn`, `LangGraphTurn`, `CodexTurn`, or `OpenAITurn` and feed it that harness's native stream.
182186

183187
### Async Temporal (auto-send)
184188

src/agentex/lib/adk/__init__.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
from agentex.lib.adk._modules.agents import AgentsModule
77
from agentex.lib.adk._modules.agent_task_tracker import AgentTaskTrackerModule
88
from agentex.lib.adk._modules.checkpointer import create_checkpointer
9-
from agentex.lib.adk._modules._langgraph_tracing import create_langgraph_tracing_handler
10-
from agentex.lib.adk._modules._langgraph_async import stream_langgraph_events
11-
from agentex.lib.adk._modules._langgraph_messages import emit_langgraph_messages
12-
from agentex.lib.adk._modules._langgraph_sync import convert_langgraph_to_agentex_events
13-
from agentex.lib.adk._modules._langgraph_turn import LangGraphTurn
9+
from agentex.lib.adk._modules._langgraph_turn import LangGraphTurn, stream_langgraph_events
10+
from agentex.lib.adk._modules._langgraph_sync import (
11+
emit_langgraph_messages,
12+
convert_langgraph_to_agentex_events,
13+
)
1414
from agentex.lib.adk._modules._pydantic_ai_async import stream_pydantic_ai_events
1515
from agentex.lib.adk._modules._pydantic_ai_sync import convert_pydantic_ai_to_agentex_events
1616
from agentex.lib.adk._modules._pydantic_ai_tracing import create_pydantic_ai_tracing_handler
@@ -68,7 +68,6 @@
6868
"agent_task_tracker",
6969
# Checkpointing / LangGraph
7070
"create_checkpointer",
71-
"create_langgraph_tracing_handler",
7271
"stream_langgraph_events",
7372
"emit_langgraph_messages",
7473
"convert_langgraph_to_agentex_events",

src/agentex/lib/adk/_modules/_langgraph_async.py

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/agentex/lib/adk/_modules/_langgraph_messages.py

Lines changed: 0 additions & 85 deletions
This file was deleted.

src/agentex/lib/adk/_modules/_langgraph_sync.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ async def convert_langgraph_to_agentex_events(
4848
Supports both regular models (chunk.content is a str) and reasoning models
4949
like gpt-5/o1/o3 (chunk.content is a list of typed content blocks).
5050
51-
AGX1-377 note: LangGraph emits tool requests as ``StreamTaskMessageFull`` (from
52-
"updates" events), NOT Start+Delta+Done like pydantic-ai. No coalesce_tool_requests
51+
LangGraph emits tool requests as ``StreamTaskMessageFull`` (from "updates"
52+
events), NOT Start+Delta+Done like pydantic-ai. No coalesce_tool_requests
5353
option is needed for LangGraph.
5454
5555
Args:
@@ -271,3 +271,82 @@ async def convert_langgraph_to_agentex_events(
271271
yield StreamTaskMessageDone(type="done", index=message_index)
272272
if reasoning_streaming:
273273
yield StreamTaskMessageDone(type="done", index=message_index)
274+
275+
276+
async def emit_langgraph_messages(messages: list[Any], task_id: str) -> str:
277+
"""Create Agentex messages for a list of LangGraph messages.
278+
279+
This is the non-streaming counterpart to ``stream_langgraph_events``. Use it
280+
when you run a LangGraph graph with ``ainvoke`` (for example a Temporal-backed
281+
agent using the LangGraph plugin, where streaming deltas aren't available) and
282+
want to surface the resulting messages to the Agentex UI after the fact.
283+
284+
It maps LangGraph/LangChain message objects to Agentex content types:
285+
286+
- ``AIMessage`` tool calls -> ``ToolRequestContent`` (one per call)
287+
- ``AIMessage`` text content -> ``TextContent``
288+
- ``ToolMessage`` -> ``ToolResponseContent``
289+
290+
Pass only the messages produced this turn (e.g. ``messages[already_emitted:]``)
291+
so each message is surfaced exactly once across a multi-turn conversation.
292+
293+
Args:
294+
messages: LangGraph/LangChain message objects to surface — typically
295+
the new messages a turn produced.
296+
task_id: The Agentex task to create messages on.
297+
298+
Returns:
299+
The last assistant text emitted (useful as a span/turn output), or "".
300+
"""
301+
# Lazy imports so langchain isn't required at module load time.
302+
from langchain_core.messages import AIMessage, ToolMessage
303+
304+
from agentex.lib import adk
305+
from agentex.types.text_content import TextContent
306+
from agentex.types.tool_request_content import ToolRequestContent
307+
from agentex.types.tool_response_content import ToolResponseContent
308+
309+
final_text = ""
310+
for message in messages:
311+
if isinstance(message, AIMessage):
312+
for tool_call in message.tool_calls or []:
313+
await adk.messages.create(
314+
task_id=task_id,
315+
content=ToolRequestContent(
316+
author="agent",
317+
tool_call_id=tool_call["id"],
318+
name=tool_call["name"],
319+
arguments=tool_call["args"],
320+
),
321+
)
322+
# ``content`` may be a plain string (OpenAI) or a list of content
323+
# blocks (Anthropic/Claude via LangChain, e.g.
324+
# ``[{"type": "text", "text": "..."}]``). Extract and join the text
325+
# so the response is visible regardless of the underlying model.
326+
if isinstance(message.content, str):
327+
text = message.content
328+
else:
329+
text = "".join(
330+
block.get("text", "") if isinstance(block, dict) else str(block)
331+
for block in message.content
332+
if not isinstance(block, dict) or block.get("type") == "text"
333+
)
334+
if text:
335+
final_text = text
336+
await adk.messages.create(
337+
task_id=task_id,
338+
content=TextContent(author="agent", content=text, format="markdown"),
339+
)
340+
elif isinstance(message, ToolMessage):
341+
await adk.messages.create(
342+
task_id=task_id,
343+
content=ToolResponseContent(
344+
author="agent",
345+
tool_call_id=message.tool_call_id,
346+
name=message.name or "unknown",
347+
content=message.content
348+
if isinstance(message.content, str)
349+
else str(message.content),
350+
),
351+
)
352+
return final_text

0 commit comments

Comments
 (0)