Skip to content

Commit b3efb55

Browse files
declan-scaleclaude
andcommitted
refactor(tutorials)!: migrate to the unified harness surface + renumber
Retire the duplicate pre-unified `harness_*` tutorials and migrate every tutorial onto the canonical unified harness surface (UnifiedEmitter / Turn / convert_* helpers). Renumber onto the `NNN_<name>` paradigm, fixing the 060/130/140 collision; codex takes fresh 070/140/150 slots. Non-breaking: the unified surface already exists; the deprecated tracing handlers are still present and are removed in a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5953df6 commit b3efb55

206 files changed

Lines changed: 678 additions & 6895 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/tutorials/00_sync/030_langgraph/README.md

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,50 @@
1-
# Tutorial 030: Sync LangGraph Agent
1+
# Tutorial: Sync LangGraph Agent
22

3-
This tutorial demonstrates how to build a **synchronous** LangGraph agent on AgentEx with:
4-
- Tool calling (ReAct pattern)
5-
- Streaming token output
6-
- Multi-turn conversation memory via AgentEx checkpointer
7-
- Tracing integration
3+
This tutorial demonstrates how to build a **synchronous** LangGraph agent on AgentEx
4+
using the **unified harness surface**:
85

9-
## Graph Structure
6+
```python
7+
turn = LangGraphTurn(stream, model=None)
8+
emitter = UnifiedEmitter(task_id=task_id, trace_id=task_id, ...)
9+
async for event in emitter.yield_turn(turn):
10+
yield event
11+
```
1012

11-
![Graph](graph.png)
13+
The `LangGraphTurn` + `UnifiedEmitter` path replaces calling the lower-level
14+
``convert_langgraph_to_agentex_events`` helper directly.
1215

1316
## Key Concepts
1417

15-
### Sync ACP
16-
The sync ACP model uses HTTP request/response for communication. The `@acp.on_message_send` handler receives a message and yields streaming events back to the client.
18+
### Unified Harness
19+
20+
`LangGraphTurn` implements the `HarnessTurn` protocol: it wraps the raw
21+
LangGraph `astream()` generator and exposes `events` (an async generator of
22+
`TaskMessageUpdate`) and `usage()` (token counts captured from the final
23+
`AIMessage`).
24+
25+
`UnifiedEmitter.yield_turn(turn)` iterates the turn's events and yields them
26+
to the sync ACP handler unchanged. The same `LangGraphTurn` object can also be
27+
passed to `UnifiedEmitter.auto_send_turn` in the async/temporal channels.
1728

18-
### LangGraph Integration
19-
- **StateGraph**: Defines the agent's state machine with `AgentState` (message history)
20-
- **ToolNode**: Automatically executes tool calls from the LLM
21-
- **tools_condition**: Routes between tool execution and final response
22-
- **Checkpointer**: Uses AgentEx's HTTP checkpointer for cross-request memory
29+
### AGX1-377 Note
2330

24-
### Streaming
25-
The agent streams tokens as they're generated using `convert_langgraph_to_agentex_events()`, which converts LangGraph's stream events into AgentEx `TaskMessageUpdate` events.
31+
LangGraph emits tool requests as `StreamTaskMessageFull` events (from "updates"
32+
node outputs). The `SpanDeriver` does not open tool spans from Full events
33+
today; that gap is tracked in AGX1-373.
2634

2735
## Files
2836

2937
| File | Description |
3038
|------|-------------|
31-
| `project/acp.py` | ACP server and message handler |
32-
| `project/graph.py` | LangGraph state graph definition |
39+
| `project/acp.py` | ACP server using unified harness (LangGraphTurn + yield_turn) |
40+
| `project/graph.py` | LangGraph state graph (weather example) |
3341
| `project/tools.py` | Tool definitions (weather example) |
3442
| `tests/test_agent.py` | Integration tests |
35-
| `manifest.yaml` | Agent configuration |
43+
| `manifest.yaml` | Agent configuration (name: s030-langgraph) |
3644

3745
## Running Locally
3846

3947
```bash
40-
# From this directory
4148
agentex agents run
4249
```
4350

-16 KB
Binary file not shown.

examples/tutorials/00_sync/030_langgraph/manifest.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ local_development:
1717
agent:
1818
acp_type: sync
1919
name: s030-langgraph
20-
description: A sync LangGraph agent with tool calling and streaming
20+
description: A sync LangGraph agent using the unified harness surface (LangGraphTurn + UnifiedEmitter.yield_turn)
2121

2222
temporal:
2323
enabled: false
@@ -47,7 +47,7 @@ deployment:
4747
global:
4848
agent:
4949
name: "s030-langgraph"
50-
description: "A sync LangGraph agent with tool calling and streaming"
50+
description: "A sync LangGraph agent using the unified harness surface"
5151
replicaCount: 1
5252
resources:
5353
requests:
Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1-
"""
2-
ACP (Agent Communication Protocol) handler for Agentex.
3-
4-
This is the API layer — it manages the graph lifecycle and streams
5-
tokens and tool calls from the LangGraph graph to the Agentex frontend.
1+
"""ACP handler for the sync LangGraph agent.
2+
3+
Uses the unified harness surface: ``LangGraphTurn`` wraps the LangGraph
4+
``astream()`` generator, and ``UnifiedEmitter.yield_turn`` converts it into
5+
the AgentEx ``TaskMessageUpdate`` event stream expected by the sync ACP.
6+
7+
Properties of the unified surface:
8+
- Tracing is wired through the tracing manager (no bespoke handler boilerplate).
9+
- No manual text-delta accumulation for the span output.
10+
- Tool calls are emitted as ``StreamTaskMessageFull`` (not Start+Delta+Done)
11+
via the same code path as the async/temporal channels.
12+
- Usage data (token counts) is captured on the ``LangGraphTurn`` object and
13+
can be read after the turn completes.
14+
15+
AGX1-377 note: LangGraph emits tool requests as ``StreamTaskMessageFull``
16+
events (from "updates"). The ``SpanDeriver`` does not open tool spans from
17+
Full events today; that gap is tracked in AGX1-373.
618
"""
719

820
from __future__ import annotations
@@ -16,29 +28,29 @@
1628

1729
import agentex.lib.adk as adk
1830
from project.graph import create_graph
19-
from agentex.lib.adk import create_langgraph_tracing_handler, convert_langgraph_to_agentex_events
2031
from agentex.lib.types.acp import SendMessageParams
2132
from agentex.lib.types.tracing import SGPTracingProcessorConfig
2233
from agentex.lib.utils.logging import make_logger
2334
from agentex.lib.sdk.fastacp.fastacp import FastACP
35+
from agentex.lib.core.harness.emitter import UnifiedEmitter
2436
from agentex.types.task_message_delta import TextDelta
2537
from agentex.types.task_message_update import TaskMessageUpdate
2638
from agentex.types.task_message_content import TaskMessageContent
39+
from agentex.lib.adk._modules._langgraph_turn import LangGraphTurn
2740
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
2841

2942
logger = make_logger(__name__)
3043

31-
# Register the Agentex tracing processor so spans are shipped to the backend
3244
add_tracing_processor_config(
3345
SGPTracingProcessorConfig(
3446
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
3547
sgp_account_id=os.environ.get("SGP_ACCOUNT_ID", ""),
3648
sgp_base_url=os.environ.get("SGP_CLIENT_BASE_URL", ""),
37-
))
38-
# Create ACP server
49+
)
50+
)
51+
3952
acp = FastACP.create(acp_type="sync")
4053

41-
# Compiled graph (lazy-initialized on first request)
4254
_graph = None
4355

4456

@@ -54,41 +66,42 @@ async def get_graph():
5466
async def handle_message_send(
5567
params: SendMessageParams,
5668
) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]:
57-
"""Handle incoming messages from Agentex, streaming tokens and tool calls."""
69+
"""Handle incoming messages, streaming tokens and tool calls via unified harness."""
5870
graph = await get_graph()
5971

60-
thread_id = params.task.id
72+
task_id = params.task.id
6173
user_message = params.content.content
6274

63-
logger.info(f"Processing message for thread {thread_id}")
75+
logger.info(f"Processing message for task {task_id}")
6476

6577
async with adk.tracing.span(
66-
trace_id=thread_id,
78+
trace_id=task_id,
79+
task_id=task_id,
6780
name="message",
6881
input={"message": user_message},
6982
data={"__span_type__": "AGENT_WORKFLOW"},
7083
) as turn_span:
71-
callback = create_langgraph_tracing_handler(
72-
trace_id=thread_id,
73-
parent_span_id=turn_span.id if turn_span else None,
74-
)
75-
7684
stream = graph.astream(
7785
{"messages": [{"role": "user", "content": user_message}]},
78-
config={
79-
"configurable": {"thread_id": thread_id},
80-
"callbacks": [callback],
81-
},
86+
config={"configurable": {"thread_id": task_id}},
8287
stream_mode=["messages", "updates"],
8388
)
8489

90+
turn = LangGraphTurn(stream, model=None)
91+
emitter = UnifiedEmitter(
92+
task_id=task_id,
93+
trace_id=task_id,
94+
parent_span_id=turn_span.id if turn_span else None,
95+
)
96+
8597
final_text = ""
86-
async for event in convert_langgraph_to_agentex_events(stream):
87-
# Accumulate text deltas for span output
98+
async for event in emitter.yield_turn(turn):
99+
# Accumulate text deltas so the span's final_output is the assistant
100+
# text (matching the async tutorial), not the usage metrics.
88101
delta = getattr(event, "delta", None)
89102
if isinstance(delta, TextDelta) and delta.text_delta:
90103
final_text += delta.text_delta
91104
yield event
92105

93106
if turn_span:
94-
turn_span.output = {"final_output": final_text}
107+
turn_span.output = {"final_output": final_text, "usage": turn.usage().model_dump()}

examples/tutorials/00_sync/030_langgraph/project/graph.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
"""
2-
LangGraph graph definition.
1+
"""LangGraph graph definition for the 030_langgraph sync agent.
32
4-
Defines the state, nodes, edges, and compiles the graph.
5-
The compiled graph is the boundary between this module and the API layer.
3+
Identical to ``030_langgraph/project/graph.py`` — the graph definition is not
4+
affected by the harness migration. Only ``acp.py`` changes.
65
"""
76

87
from __future__ import annotations
@@ -35,15 +34,12 @@
3534

3635
class AgentState(TypedDict):
3736
"""State schema for the agent graph."""
37+
3838
messages: Annotated[list[Any], add_messages]
3939

4040

4141
async def create_graph():
42-
"""Create and compile the agent graph with checkpointer.
43-
44-
Returns:
45-
A compiled LangGraph StateGraph ready for invocation.
46-
"""
42+
"""Create and compile the agent graph with checkpointer."""
4743
llm = ChatOpenAI(
4844
model=MODEL_NAME,
4945
reasoning={"effort": "high", "summary": "auto"},
@@ -56,9 +52,7 @@ def agent_node(state: AgentState) -> dict[str, Any]:
5652
"""Process the current state and generate a response."""
5753
messages = state["messages"]
5854
if not messages or not isinstance(messages[0], SystemMessage):
59-
system_content = SYSTEM_PROMPT.format(
60-
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
61-
)
55+
system_content = SYSTEM_PROMPT.format(timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
6256
messages = [SystemMessage(content=system_content)] + messages
6357
response = llm_with_tools.invoke(messages)
6458
return {"messages": [response]}
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
"""
2-
Tool definitions for the LangGraph agent.
3-
4-
Add your custom tools here. Each tool should be a function decorated with @tool
5-
or created using the Tool class.
6-
"""
1+
"""Tool definitions for the 030_langgraph sync agent."""
72

83
from langchain_core.tools import Tool
94

@@ -17,16 +12,13 @@ def get_weather(city: str) -> str:
1712
Returns:
1813
A string describing the weather conditions.
1914
"""
20-
# TODO: Replace with actual weather API call
2115
return f"The weather in {city} is sunny and 72°F"
2216

2317

24-
# Define tools
2518
weather_tool = Tool(
2619
name="get_weather",
2720
func=get_weather,
2821
description="Get the current weather for a city. Input should be a city name.",
2922
)
3023

31-
# Export all tools as a list
3224
TOOLS = [weather_tool]

examples/tutorials/00_sync/030_langgraph/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
55
[project]
66
name = "s030-langgraph"
77
version = "0.1.0"
8-
description = "A sync LangGraph agent with tool calling and streaming"
8+
description = "A sync LangGraph agent using the unified harness surface"
99
readme = "README.md"
1010
requires-python = ">=3.12"
1111
dependencies = [

0 commit comments

Comments
 (0)