Skip to content

Commit fb4111f

Browse files
declan-scaleclaude
andcommitted
refactor(cli): migrate existing langgraph/pydantic-ai templates to unified surface
Update the scaffolded acp.py / agent.py for default-langgraph, default-pydantic-ai, sync-langgraph, sync-pydantic-ai and temporal-pydantic-ai to the canonical unified harness surface, matching the migrated tutorials. Promote LangGraphTurn and PydanticAITurn to the public agentex.lib.adk facade (parallel to ClaudeCodeTurn / CodexTurn) so the scaffolded user code imports them from the stable public path instead of a private _modules path. Non-breaking: template scaffolding + additive public exports only. The unified surface already exists; deprecated tracing handlers are removed in follow-ups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1e6b84c commit fb4111f

6 files changed

Lines changed: 62 additions & 61 deletions

File tree

src/agentex/lib/adk/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
from agentex.lib.adk._modules._langgraph_async import stream_langgraph_events
1111
from agentex.lib.adk._modules._langgraph_messages import emit_langgraph_messages
1212
from agentex.lib.adk._modules._langgraph_sync import convert_langgraph_to_agentex_events
13+
from agentex.lib.adk._modules._langgraph_turn import LangGraphTurn
1314
from agentex.lib.adk._modules._pydantic_ai_async import stream_pydantic_ai_events
1415
from agentex.lib.adk._modules._pydantic_ai_sync import convert_pydantic_ai_to_agentex_events
1516
from agentex.lib.adk._modules._pydantic_ai_tracing import create_pydantic_ai_tracing_handler
17+
from agentex.lib.adk._modules._pydantic_ai_turn import PydanticAITurn
1618
from agentex.lib.adk._modules._claude_code_sync import convert_claude_code_to_agentex_events
1719
from agentex.lib.adk._modules._claude_code_turn import (
1820
ClaudeCodeTurn,
@@ -70,10 +72,12 @@
7072
"stream_langgraph_events",
7173
"emit_langgraph_messages",
7274
"convert_langgraph_to_agentex_events",
75+
"LangGraphTurn",
7376
# Pydantic AI
7477
"stream_pydantic_ai_events",
7578
"convert_pydantic_ai_to_agentex_events",
7679
"create_pydantic_ai_tracing_handler",
80+
"PydanticAITurn",
7781
# Claude Code
7882
"convert_claude_code_to_agentex_events",
7983
"ClaudeCodeTurn",

src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ if _litellm_key:
1515
os.environ["OPENAI_API_KEY"] = _litellm_key
1616

1717
import agentex.lib.adk as adk
18-
from agentex.lib.adk import create_langgraph_tracing_handler, stream_langgraph_events
18+
from agentex.lib.core.harness import UnifiedEmitter
1919
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
2020
from agentex.lib.sdk.fastacp.fastacp import FastACP
2121
from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams
2222
from agentex.lib.types.fastacp import AsyncACPConfig
2323
from agentex.lib.types.tracing import SGPTracingProcessorConfig
2424
from agentex.lib.utils.logging import make_logger
25+
from agentex.lib.adk import LangGraphTurn
2526

2627
from project.graph import create_graph
2728

@@ -67,24 +68,23 @@ async def handle_task_event_send(params: SendEventParams):
6768
input={"message": user_message},
6869
data={"__span_type__": "AGENT_WORKFLOW"},
6970
) as turn_span:
70-
callback = create_langgraph_tracing_handler(
71-
trace_id=task_id,
72-
parent_span_id=turn_span.id if turn_span else None,
73-
)
74-
7571
stream = graph.astream(
7672
{"messages": [{"role": "user", "content": user_message}]},
77-
config={
78-
"configurable": {"thread_id": task_id},
79-
"callbacks": [callback],
80-
},
73+
config={"configurable": {"thread_id": task_id}},
8174
stream_mode=["messages", "updates"],
8275
)
8376

84-
final_output = await stream_langgraph_events(stream, task_id)
77+
turn = LangGraphTurn(stream, model=None)
78+
emitter = UnifiedEmitter(
79+
task_id=task_id,
80+
trace_id=task_id,
81+
parent_span_id=turn_span.id if turn_span else None,
82+
)
83+
84+
result = await emitter.auto_send_turn(turn)
8585

8686
if turn_span:
87-
turn_span.output = {"final_output": final_output}
87+
turn_span.output = {"final_output": result.final_text}
8888

8989

9090
@acp.on_task_create

src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,19 @@ from dotenv import load_dotenv
1919

2020
load_dotenv()
2121

22-
from project.agent import create_agent
22+
from project.agent import MODEL_NAME, create_agent
2323
from pydantic_ai.run import AgentRunResultEvent
2424
from pydantic_ai.messages import ModelMessagesTypeAdapter
2525

2626
import agentex.lib.adk as adk
27-
from agentex.lib.adk import (
28-
stream_pydantic_ai_events,
29-
create_pydantic_ai_tracing_handler,
30-
)
3127
from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams
28+
from agentex.lib.core.harness import UnifiedEmitter
3229
from agentex.lib.types.fastacp import AsyncACPConfig
3330
from agentex.lib.types.tracing import SGPTracingProcessorConfig
3431
from agentex.lib.utils.logging import make_logger
3532
from agentex.lib.utils.model_utils import BaseModel
3633
from agentex.lib.sdk.fastacp.fastacp import FastACP
34+
from agentex.lib.adk import PydanticAITurn
3735
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
3836

3937
logger = make_logger(__name__)
@@ -125,15 +123,17 @@ async def handle_task_event_send(params: SendEventParams):
125123
input={"message": user_message},
126124
data={"__span_type__": "AGENT_WORKFLOW"},
127125
) as turn_span:
128-
tracing_handler = create_pydantic_ai_tracing_handler(
126+
# Construct the UnifiedEmitter from the ACP context so tracing is
127+
# automatic and messages are auto-sent to the task stream (Redis).
128+
emitter = UnifiedEmitter(
129+
task_id=task_id,
129130
trace_id=task_id,
130131
parent_span_id=turn_span.id if turn_span else None,
131-
task_id=task_id,
132132
)
133133

134134
# Wrap the pydantic-ai event stream so we can capture the final
135135
# AgentRunResultEvent (which carries the full message list for the
136-
# next turn) without changing the streaming-helper's signature.
136+
# next turn) before forwarding events to the emitter.
137137
captured_messages: list[Any] = []
138138

139139
async def tee_messages(upstream) -> AsyncIterator[Any]:
@@ -143,9 +143,8 @@ async def handle_task_event_send(params: SendEventParams):
143143
yield event
144144

145145
async with agent.run_stream_events(user_message, message_history=previous_messages) as stream:
146-
final_output = await stream_pydantic_ai_events(
147-
tee_messages(stream), task_id, tracing_handler=tracing_handler
148-
)
146+
turn = PydanticAITurn(tee_messages(stream), model=MODEL_NAME)
147+
result = await emitter.auto_send_turn(turn)
149148

150149
# Save the updated message history so the next turn picks up here.
151150
if captured_messages:
@@ -158,7 +157,7 @@ async def handle_task_event_send(params: SendEventParams):
158157
)
159158

160159
if turn_span:
161-
turn_span.output = {"final_output": final_output}
160+
turn_span.output = {"final_output": result.final_text}
162161

163162

164163
@acp.on_task_cancel

src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ tokens and tool calls from the LangGraph graph to the Agentex frontend.
88
from typing import AsyncGenerator
99

1010
import agentex.lib.adk as adk
11-
from agentex.lib.adk import create_langgraph_tracing_handler, convert_langgraph_to_agentex_events
11+
from agentex.lib.core.harness import UnifiedEmitter
1212
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
1313
from agentex.lib.sdk.fastacp.fastacp import FastACP
1414
from agentex.protocol.acp import SendMessageParams
1515
from agentex.lib.types.tracing import SGPTracingProcessorConfig
1616
from agentex.lib.utils.logging import make_logger
17+
from agentex.lib.adk import LangGraphTurn
1718
from agentex.types.task_message_content import TaskMessageContent
1819
from agentex.types.task_message_delta import TextDelta
1920
from agentex.types.task_message_update import TaskMessageUpdate
@@ -72,22 +73,21 @@ async def handle_message_send(
7273
input={"message": user_message},
7374
data={"__span_type__": "AGENT_WORKFLOW"},
7475
) as turn_span:
75-
callback = create_langgraph_tracing_handler(
76-
trace_id=thread_id,
77-
parent_span_id=turn_span.id if turn_span else None,
78-
)
79-
8076
stream = graph.astream(
8177
{"messages": [{"role": "user", "content": user_message}]},
82-
config={
83-
"configurable": {"thread_id": thread_id},
84-
"callbacks": [callback],
85-
},
78+
config={"configurable": {"thread_id": thread_id}},
8679
stream_mode=["messages", "updates"],
8780
)
8881

82+
turn = LangGraphTurn(stream, model=None)
83+
emitter = UnifiedEmitter(
84+
task_id=thread_id,
85+
trace_id=thread_id,
86+
parent_span_id=turn_span.id if turn_span else None,
87+
)
88+
8989
final_text = ""
90-
async for event in convert_langgraph_to_agentex_events(stream):
90+
async for event in emitter.yield_turn(turn):
9191
# Accumulate text deltas for span output
9292
delta = getattr(event, "delta", None)
9393
if isinstance(delta, TextDelta) and delta.text_delta:

src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,17 @@ from dotenv import load_dotenv
1515

1616
load_dotenv()
1717

18-
from project.agent import create_agent
18+
from project.agent import MODEL_NAME, create_agent
1919

2020
import agentex.lib.adk as adk
21-
from agentex.lib.adk import (
22-
create_pydantic_ai_tracing_handler,
23-
convert_pydantic_ai_to_agentex_events,
24-
)
2521
from agentex.protocol.acp import SendMessageParams
22+
from agentex.lib.core.harness import UnifiedEmitter
2623
from agentex.lib.types.tracing import SGPTracingProcessorConfig
2724
from agentex.lib.utils.logging import make_logger
2825
from agentex.lib.sdk.fastacp.fastacp import FastACP
2926
from agentex.types.task_message_update import TaskMessageUpdate
3027
from agentex.types.task_message_content import TaskMessageContent
28+
from agentex.lib.adk import PydanticAITurn
3129
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
3230

3331
logger = make_logger(__name__)
@@ -73,21 +71,22 @@ async def handle_message_send(
7371
logger.info(f"Processing message for task {task_id}")
7472

7573
# Open a per-message turn span. Tool calls below nest underneath this
76-
# span via the tracing handler's parent_span_id wiring.
74+
# span via the emitter's parent_span_id wiring.
7775
async with adk.tracing.span(
7876
trace_id=task_id,
7977
task_id=task_id,
8078
name="message",
8179
input={"message": user_message},
8280
data={"__span_type__": "AGENT_WORKFLOW"},
8381
) as turn_span:
84-
tracing_handler = create_pydantic_ai_tracing_handler(
82+
# Construct the UnifiedEmitter from the ACP/streaming context so tracing
83+
# is automatic: tool spans nest under this turn's span.
84+
emitter = UnifiedEmitter(
85+
task_id=task_id,
8586
trace_id=task_id,
8687
parent_span_id=turn_span.id if turn_span else None,
87-
task_id=task_id,
8888
)
8989
async with agent.run_stream_events(user_message) as stream:
90-
async for event in convert_pydantic_ai_to_agentex_events(
91-
stream, tracing_handler=tracing_handler
92-
):
93-
yield event
90+
turn = PydanticAITurn(stream, model=MODEL_NAME)
91+
async for ev in emitter.yield_turn(turn):
92+
yield ev

src/agentex/lib/cli/templates/temporal-pydantic-ai/project/agent.py.j2

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ moves into recorded activities.
1111

1212
Streaming back to Agentex happens via ``event_stream_handler``, which
1313
receives Pydantic AI ``AgentStreamEvent``s from inside the model activity
14-
and forwards them to Redis using the ``stream_pydantic_ai_events`` helper.
15-
The ``task_id`` and tracing parent span ID are threaded into the handler
16-
via ``deps``.
14+
and forwards them through the unified harness surface
15+
(``UnifiedEmitter.auto_send_turn`` + ``PydanticAITurn``). The ``task_id`` and
16+
tracing parent span ID are threaded into the handler via ``deps``.
1717
"""
1818

1919
from __future__ import annotations
@@ -27,10 +27,8 @@ from project.tools import get_weather
2727
from pydantic_ai.messages import AgentStreamEvent
2828
from pydantic_ai.durable_exec.temporal import TemporalAgent
2929

30-
from agentex.lib.adk import (
31-
stream_pydantic_ai_events,
32-
create_pydantic_ai_tracing_handler,
33-
)
30+
from agentex.lib.core.harness import UnifiedEmitter
31+
from agentex.lib.adk import PydanticAITurn
3432

3533
# Swap this for any Pydantic AI-supported model identifier
3634
# (e.g. "anthropic:claude-3-5-sonnet-latest", "openai:gpt-4o").
@@ -92,17 +90,18 @@ async def event_handler(
9290
activity (not the workflow), it can freely make non-deterministic Redis
9391
writes — including the tracing HTTP calls that record per-tool-call
9492
spans under the workflow's per-turn span (when ``parent_span_id`` is set).
93+
94+
The UnifiedEmitter is constructed from ``deps`` (task_id + parent_span_id),
95+
so tool spans nest under the workflow's per-turn span and messages auto-send
96+
to the task stream.
9597
"""
96-
tracing_handler = create_pydantic_ai_tracing_handler(
98+
emitter = UnifiedEmitter(
99+
task_id=run_context.deps.task_id,
97100
trace_id=run_context.deps.task_id,
98101
parent_span_id=run_context.deps.parent_span_id,
99-
task_id=run_context.deps.task_id,
100-
)
101-
await stream_pydantic_ai_events(
102-
events,
103-
run_context.deps.task_id,
104-
tracing_handler=tracing_handler,
105102
)
103+
turn = PydanticAITurn(events, model=MODEL_NAME)
104+
await emitter.auto_send_turn(turn)
106105

107106

108107
# Construct the durable agent at module load time so that the

0 commit comments

Comments
 (0)