Skip to content

Commit 31fd697

Browse files
authored
docs(streaming): document the unified harness surface (#337)
1 parent acbe9e3 commit 31fd697

4 files changed

Lines changed: 128 additions & 2 deletions

File tree

agentex/docs/docs/development_guides/streaming_patterns.md

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,75 @@ async def streaming_with_override(params: SendMessageParams) -> AsyncGenerator[T
266266
- **Analysis**: Stream thinking process, then replace with final conclusion
267267
- **Tool integration**: Stream partial results, then override with tool-enhanced content
268268

269+
## Unified Harness Surface (Framework Agents)
270+
271+
!!! tip "Recommended for framework-based agents"
272+
If your agent is built on an agent framework (LangGraph, Pydantic AI, OpenAI Agents SDK) or a coding CLI (Claude Code, Codex), the **unified harness surface** is the recommended way to stream. You write nothing ACP-specific: wrap the framework's native event stream in a `HarnessTurn` and hand it to a `UnifiedEmitter`. The emitter delivers the same canonical `StreamTaskMessage*` stream over either channel **and derives tracing spans from it automatically** — no separate tracing handler.
273+
274+
The canonical event stream (`StreamTaskMessageStart/Delta/Full/Done`) is produced **once** by a per-framework *tap* (`convert_<framework>_to_agentex_events`). The emitter consumes it and handles delivery + tracing uniformly, so the same `turn` object works for Sync, Async, and Temporal — only the delivery call changes.
275+
276+
Per-framework `HarnessTurn` wrappers ship today, all re-exported from `agentex.lib.adk` (requires `agentex-sdk >= 0.14.0`):
277+
278+
| Framework | Turn class | Tap function |
279+
|---|---|---|
280+
| Pydantic AI | `PydanticAITurn` | `convert_pydantic_ai_to_agentex_events` |
281+
| LangGraph | `LangGraphTurn` | `convert_langgraph_to_agentex_events` |
282+
| OpenAI Agents | `OpenAITurn` | `convert_openai_to_agentex_events` |
283+
| Claude Code | `ClaudeCodeTurn` | `convert_claude_code_to_agentex_events` |
284+
| Codex | `CodexTurn` | `convert_codex_to_agentex_events` |
285+
286+
```python
287+
import agentex.lib.adk as adk
288+
from agentex.lib.adk import UnifiedEmitter, LangGraphTurn # swap for any Turn above
289+
290+
# Sync ACP — yield over the HTTP response:
291+
@acp.on_message_send
292+
async def handle_message_send(params: SendMessageParams):
293+
task_id = params.task.id
294+
async with adk.tracing.span(
295+
trace_id=task_id, task_id=task_id, name="message",
296+
data={"__span_type__": "AGENT_WORKFLOW"},
297+
) as turn_span:
298+
emitter = UnifiedEmitter(
299+
task_id=task_id, trace_id=task_id,
300+
parent_span_id=turn_span.id if turn_span else None,
301+
)
302+
turn = LangGraphTurn(graph.astream(...)) # feed the framework's native stream
303+
async for event in emitter.yield_turn(turn):
304+
yield event
305+
306+
# Async ACP — push to Redis, get final text + usage back:
307+
@acp.on_task_event_send
308+
async def handle_event_send(params: SendEventParams):
309+
task_id = params.task.id
310+
async with adk.tracing.span(
311+
trace_id=task_id, task_id=task_id, name="turn",
312+
data={"__span_type__": "AGENT_WORKFLOW"},
313+
) as turn_span:
314+
emitter = UnifiedEmitter(
315+
task_id=task_id, trace_id=task_id,
316+
parent_span_id=turn_span.id if turn_span else None,
317+
)
318+
turn = LangGraphTurn(graph.astream(...))
319+
result = await emitter.auto_send_turn(turn) # auto_send_turn replaces yield_turn
320+
# result.final_text, result.usage
321+
```
322+
323+
In a **Temporal** workflow the body is the same — construct the `UnifiedEmitter`, build the `turn`, and `await emitter.auto_send_turn(turn, created_at=workflow.now())` (pass `created_at` so events carry deterministic workflow time).
324+
325+
**Why use it:**
326+
327+
- **One mental model** for Sync, Async, and Temporal — `yield_turn` vs `auto_send_turn`, same `turn`.
328+
- **Tracing for free** — tool and reasoning spans are derived from the canonical stream (tool spans paired by `tool_call_id`) and fan out to every registered processor. No `create_<framework>_tracing_handler` to wire (those per-framework tracing handlers have been removed).
329+
- **Usage + final text**`auto_send_turn` returns a `TurnResult` (`final_text`, `usage`) with normalized token/cost numbers.
330+
- **Streamed tool args survive async** — the emitter honors streamed `ToolRequestDelta`s on the Redis channel, not just sync.
331+
332+
To add a framework that doesn't have a `HarnessTurn` yet, follow the `agentex-add-agent-framework` workflow: write a tap + a `<Fw>Turn`, export both from `agentex.lib.adk`. There is no per-framework async streamer or tracing handler to author.
333+
269334
## OpenAI Agents SDK Streaming
270335

271-
!!! note "More Modular Approach"
272-
The **OpenAI Agents SDK** provides a more modular alternative to `auto_send`. Instead of black-box logic, you write custom **providers** (for sync agents) and/or **hooks + providers** (for agentic ACPs) by inheriting classes and overriding methods. This Pythonic approach gives you full control over streaming behavior while working with **any LiteLLM model** (GPT-4, Claude, Gemini, etc.).
336+
!!! note "Lower-level alternative for fine-grained OpenAI SDK control"
337+
The unified harness `OpenAITurn` above covers most OpenAI Agents SDK agents. The custom **provider** (sync) and **hooks + provider** (agentic) approach below is the lower-level path when you need to override streaming behavior directly — it works with **any LiteLLM model** (GPT-4, Claude, Gemini, etc.).
273338

274339
The OpenAI Agents SDK supports [any LiteLLM model](https://openai.github.io/openai-agents-python/models/litellm/) and provides a cleaner architecture than `auto_send`:
275340

@@ -516,6 +581,10 @@ graph TB
516581

517582
!!! info "Choose the Right Pattern"
518583

584+
**Framework / coding-CLI agents (Recommended): Unified Harness Surface**
585+
- Wrap the framework's stream in a `HarnessTurn` (`LangGraphTurn`, `PydanticAITurn`, `OpenAITurn`, `ClaudeCodeTurn`, `CodexTurn`) and deliver via `UnifiedEmitter`.
586+
- `yield_turn` for Sync, `auto_send_turn` for Async/Temporal — same `turn`, tracing derived automatically.
587+
519588
**Standard LiteLLM Providers (via ADK):**
520589
- **Sync ACP**: Manual delta accumulation, full control
521590
- **Async Auto Send**: Automatic but black-box (easiest, least flexible)

agentex/docs/docs/development_guides/tutorials.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ Basic agent patterns with simple request-response interactions.
1414
- **[Multiturn](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/010_multiturn){target="_blank"}**: Conversation history and memory
1515
- **[Streaming](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/020_streaming){target="_blank"}**: Real-time response streaming
1616

17+
Framework starters (the `agentex init` Sync ACP options, each on the [unified harness](streaming_patterns.md#unified-harness-surface-framework-agents)):
18+
19+
- **[LangGraph](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/030_langgraph){target="_blank"}**
20+
- **[Pydantic AI](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/040_pydantic_ai){target="_blank"}**
21+
- **[OpenAI Agents SDK](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/050_openai_agents){target="_blank"}**
22+
- **[Claude Code](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/060_claude_code){target="_blank"}**
23+
- **[Codex](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/00_sync/070_codex){target="_blank"}**
24+
1725
### Async ACP - Base (Learning & Development)
1826

1927
Stateful workflows with full lifecycle control for development and advanced patterns.
@@ -26,6 +34,14 @@ Stateful workflows with full lifecycle control for development and advanced patt
2634
- **[Batch Events](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/00_base/080_batch_events){target="_blank"}**: Efficient multi-event handling
2735
- **[Multi-Agent Assembly Line](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/00_base/090_multi_agent_non_temporal){target="_blank"}**: Multi-agent coordination without Temporal
2836

37+
Framework starters (the `agentex init` Async - ACP Only options, each on the [unified harness](streaming_patterns.md#unified-harness-surface-framework-agents)):
38+
39+
- **[LangGraph](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/00_base/100_langgraph){target="_blank"}**
40+
- **[Pydantic AI](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/00_base/110_pydantic_ai){target="_blank"}**
41+
- **[OpenAI Agents SDK](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/00_base/120_openai_agents){target="_blank"}**
42+
- **[Claude Code](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/00_base/130_claude_code){target="_blank"}**
43+
- **[Codex](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/00_base/140_codex){target="_blank"}**
44+
2945
### Async ACP - Temporal (Production)
3046

3147
Enterprise-ready patterns with durable execution for production deployments requiring reliability and fault tolerance.
@@ -40,6 +56,14 @@ Enterprise-ready patterns with durable execution for production deployments requ
4056
- [OpenAI Agents SDK: Tools](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/10_temporal/070_open_ai_agents_sdk_tools){target="_blank"}: Single and multi-activity tool patterns
4157
- [OpenAI Agents SDK: Human-in-the-Loop](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/10_temporal/080_open_ai_agents_sdk_human_in_the_loop){target="_blank"}: Human approval workflows
4258

59+
Framework starters (the `agentex init` Async - Temporal options, each on the [unified harness](streaming_patterns.md#unified-harness-surface-framework-agents)):
60+
61+
- **[Pydantic AI](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/10_temporal/110_pydantic_ai){target="_blank"}**
62+
- **[OpenAI Agents SDK](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/10_temporal/120_openai_agents){target="_blank"}**
63+
- **[LangGraph](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/10_temporal/130_langgraph){target="_blank"}**
64+
- **[Claude Code](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/10_temporal/140_claude_code){target="_blank"}**
65+
- **[Codex](https://github.com/scaleapi/scale-agentex-python/tree/main/examples/tutorials/10_async/10_temporal/150_codex){target="_blank"}**
66+
4367
## Why Tutorials are on GitHub
4468

4569
**Runnable code**: Clone the repository and run examples immediately

agentex/docs/docs/getting_started/choose_your_agent_type.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,34 @@ Agentex supports three agent types with different execution models and capabilit
2121

2222
---
2323

24+
## Pick a Framework (Harness)
25+
26+
Choosing an agent type is only the **first** prompt in `agentex init`. After you pick Sync / Async-base / Temporal, the CLI asks which **agent framework** to scaffold. This is independent of the agent type — each type offers the same framework starters:
27+
28+
| `agentex init` prompt | Framework options |
29+
|---|---|
30+
| **Sync ACP** | Basic · OpenAI Agents SDK (Recommended) · OpenAI Agents SDK + Local Sandbox · LangGraph · Pydantic AI · Claude Code · Codex |
31+
| **Async - ACP Only** | Basic · OpenAI Agents SDK · LangGraph · Pydantic AI · Claude Code · Codex |
32+
| **Async - Temporal** | Basic · OpenAI Agents SDK (Recommended) · Pydantic AI · LangGraph · Claude Code · Codex |
33+
34+
!!! note "OpenAI Agents SDK + Local Sandbox"
35+
The **Local Sandbox** variant is the OpenAI Agents SDK starter wired to run tools inside a local sandbox. It shares the same harness wiring and tutorial base as the plain [OpenAI Agents SDK](../development_guides/tutorials.md#sync-acp-simple-agents) starter — there is no separate tutorial for it.
36+
37+
**What "Basic" gives you:** a blank handler that you fill in yourself (the examples in the [Project Structure Guide](project_structure.md) show this variant). Pick it when you're writing your agent loop by hand or calling an LLM through LiteLLM directly.
38+
39+
**What a framework template gives you:** the same project layout **plus** the [unified harness](../development_guides/streaming_patterns.md#unified-harness-surface-framework-agents) wiring already in place — the framework's stream wrapped in a `HarnessTurn` and delivered by `UnifiedEmitter`, so streaming and tracing work out of the box. Some frameworks also add helper files (e.g. `agent.py` + `tools.py` for Pydantic AI / OpenAI Agents, `graph.py` + `tools.py` for LangGraph).
40+
41+
How to choose:
42+
43+
- **Writing the loop yourself / LiteLLM only** → Basic.
44+
- **Already standardized on a framework** → pick it directly (LangGraph, Pydantic AI, OpenAI Agents SDK).
45+
- **Wrapping a coding CLI** → Claude Code (spawns the `claude` CLI) or Codex (spawns the `codex` CLI) as a local subprocess, streamed through the harness.
46+
- **Want tools + good defaults and unsure** → OpenAI Agents SDK (the Recommended option for Sync and Temporal).
47+
48+
To add a framework that isn't in the list, see the `agentex-add-agent-framework` workflow.
49+
50+
---
51+
2452
## Upgrade Path
2553

2654
| Current Type | When to Upgrade | Upgrade To | Key Benefit |

agentex/docs/docs/getting_started/project_structure.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ When you run `agentex init`, the CLI creates a complete agent project with every
1010
- **Async Base Agent** - Custom async implementations
1111
- **Async Temporal Agent** - Complex, durable workflows
1212

13+
`agentex init` then asks which **agent framework** to scaffold (Basic, OpenAI Agents SDK, LangGraph, Pydantic AI, Claude Code, or Codex) — see [Pick a Framework (Harness)](choose_your_agent_type.md#pick-a-framework-harness).
14+
15+
!!! note "These examples are the **Basic** variant"
16+
The `project/acp.py` shown for each agent type below is the **Basic** (blank-slate) template. If you pick a framework instead, the handler comes pre-wired with the [unified harness](../development_guides/streaming_patterns.md#unified-harness-surface-framework-agents) (the framework's stream wrapped in a `HarnessTurn`, delivered by `UnifiedEmitter`, with streaming + tracing handled), and some frameworks add helper files: `agent.py` + `tools.py` (Pydantic AI, OpenAI Agents) or `graph.py` + `tools.py` (LangGraph). For **Sync** and **Async-base** agents this wiring lives in `acp.py`; for **Temporal** agents it lives in `workflow.py` / `activities.py` instead. The rest of the layout is identical.
17+
1318
This guide is organized by agent type. Jump to your section:
1419

1520
- [Sync Agent Structure](#sync-agent-structure)

0 commit comments

Comments
 (0)