Skip to content

Added Pydantic AI sync, async, temporal integration#359

Open
michael-chou359 wants to merge 4 commits into
mainfrom
mc/pydantic-ai-converter
Open

Added Pydantic AI sync, async, temporal integration#359
michael-chou359 wants to merge 4 commits into
mainfrom
mc/pydantic-ai-converter

Conversation

@michael-chou359
Copy link
Copy Markdown

@michael-chou359 michael-chou359 commented May 18, 2026

Greptile Summary

This PR adds Pydantic AI integration to the Agentex SDK — a sync HTTP-streaming converter (convert_pydantic_ai_to_agentex_events), an async Redis-streaming helper (stream_pydantic_ai_events), a shared tracing handler, and three complete tutorial examples (sync, async-base, and async-Temporal). It also refactors CoalescingBuffer.close() in streaming.py to stop the flush task by setting _closed and awaiting natural exit rather than cancelling mid-flush, eliminating the duplicate-tail publish on stream close.

  • Sync converter (_pydantic_ai_sync.py): maps PartStartEvent, PartDeltaEvent, PartEndEvent, and FunctionToolResultEvent to StreamTaskMessageStart/Delta/Full/Done; supports streaming tool-call argument deltas. A previously-flagged P1 remains: the ThinkingPart start event emits TextContent instead of ReasoningContent while subsequent deltas use ReasoningContentDelta, causing a type mismatch.
  • Async helper (_pydantic_ai_async.py): correctly uses ReasoningContent for thinking parts; pushes events to Redis via adk.streaming/adk.messages.
  • Tracing handler (_pydantic_ai_tracing.py): uses uuid5-derived deterministic span IDs so on_tool_start / on_tool_end can pair correctly across Temporal activity boundaries without shared in-memory state.
  • Temporal tutorial (workflow.py / agent.py): shows a full durable-agent pattern where a TemporalAgent event stream handler streams tokens to Redis from inside model activities.

Confidence Score: 4/5

Safe to merge with caution — the ThinkingPart content-type mismatch in the sync converter is still unaddressed and will cause reasoning content to render incorrectly on any client that dispatches on the start-event content type.

The sync converter starts a ThinkingPart message with TextContent but immediately sends ReasoningContentDelta deltas. Clients that switch rendering mode based on the Start event content type will display thinking tokens as a plain text bubble rather than a collapsible reasoning block. Everything else looks well-structured and correct.

src/agentex/lib/adk/_modules/_pydantic_ai_sync.py — the ThinkingPart start event still emits TextContent instead of ReasoningContent

Important Files Changed

Filename Overview
src/agentex/lib/adk/_modules/_pydantic_ai_sync.py Sync event converter that maps Pydantic AI AgentStreamEvents to Agentex StreamTaskMessage* events; contains an existing unfixed P1 where ThinkingPart start uses TextContent instead of ReasoningContent
src/agentex/lib/adk/_modules/_pydantic_ai_async.py Async Redis-based streaming helper; correctly uses ReasoningContent for ThinkingPart events; contains a stale lazy-import comment
src/agentex/lib/adk/_modules/_pydantic_ai_tracing.py New tracing handler that records Agentex spans for pydantic-ai tool calls; uses deterministic UUID5-based span IDs to handle cross-activity pairing in Temporal workflows
src/agentex/lib/core/services/adk/streaming.py CoalescingBuffer refactor: removes CancelledError re-enqueue logic in favor of setting _closed and awaiting natural task exit, eliminating duplicate-tail publishes on stream close
examples/tutorials/10_async/10_temporal/110_pydantic_ai/project/agent.py Constructs a TemporalAgent with an event_stream_handler that streams pydantic-ai events to Redis via stream_pydantic_ai_events inside the model activity
examples/tutorials/10_async/10_temporal/110_pydantic_ai/project/workflow.py Temporal workflow wiring; delegates agent runs to TemporalAgent with per-turn tracing spans and no explicit activity list (uses PydanticAIPlugin auto-discovery)

Sequence Diagram

sequenceDiagram
    participant Client as HTTP or Signal
    participant ACP as FastACP Handler
    participant PA as Pydantic AI Agent
    participant Converter as Sync or Async Converter
    participant Redis as Redis via adk.streaming
    participant Tracing as Agentex Tracing API

    Client->>ACP: task event or message
    ACP->>PA: agent.run_stream_events(message)
    loop Per part
        PA-->>Converter: PartStartEvent Text or Thinking or ToolCall
        Converter->>Redis: StreamTaskMessageStart
        PA-->>Converter: PartDeltaEvent
        Converter->>Redis: StreamTaskMessageDelta
        PA-->>Converter: PartEndEvent
        Converter->>Redis: StreamTaskMessageDone
        alt ToolCallPart ended
            Converter->>Tracing: on_tool_start span create
        end
    end
    PA-->>Converter: FunctionToolResultEvent
    Converter->>Redis: StreamTaskMessageFull ToolResponseContent
    Converter->>Tracing: on_tool_end span patch end_time
    PA-->>Converter: AgentRunResultEvent ignored
    Converter->>ACP: final_text returned
Loading

Comments Outside Diff (1)

  1. src/agentex/lib/adk/_modules/_pydantic_ai_sync.py, line 431-450 (link)

    P1 ThinkingPart start event uses TextContent instead of ReasoningContent

    The sync converter emits StreamTaskMessageStart(content=TextContent(...)) for ThinkingPart events (line 434–440), but immediately follows with StreamTaskMessageDelta(delta=ReasoningContentDelta(...)) deltas. The async counterpart (_pydantic_ai_async.py) consistently uses ReasoningContent as the initial content type for the same event. The type mismatch — a TextContent start receiving ReasoningContentDelta updates — means any server-side logic that requires the accumulated message type to match the delta type will either fail silently or render thinking content as a plain text bubble rather than a collapsible reasoning block. ReasoningContent is not imported in this file at all, confirming the type was never set intentionally.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agentex/lib/adk/_modules/_pydantic_ai_sync.py
    Line: 431-450
    
    Comment:
    **ThinkingPart start event uses `TextContent` instead of `ReasoningContent`**
    
    The sync converter emits `StreamTaskMessageStart(content=TextContent(...))` for `ThinkingPart` events (line 434–440), but immediately follows with `StreamTaskMessageDelta(delta=ReasoningContentDelta(...))` deltas. The async counterpart (`_pydantic_ai_async.py`) consistently uses `ReasoningContent` as the initial content type for the same event. The type mismatch — a `TextContent` start receiving `ReasoningContentDelta` updates — means any server-side logic that requires the accumulated message type to match the delta type will either fail silently or render thinking content as a plain text bubble rather than a collapsible reasoning block. `ReasoningContent` is not imported in this file at all, confirming the type was never set intentionally.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Cursor Fix in Claude Code Fix in Codex

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/agentex/lib/adk/_modules/_pydantic_ai_async.py:90-92
**Stale "lazy import" comment**

The comment says pydantic-ai isn't required at module load time, but `pydantic-ai-slim` is now a required dependency in `pyproject.toml`. More importantly, `_pydantic_ai_sync.py` already imports `pydantic_ai` at the module level, and both modules are imported in `adk/__init__.py`, so the guarantee described here was already broken. The comment is misleading and should be removed.

Reviews (4): Last reviewed commit: "added tracing" | Re-trigger Greptile

@michael-chou359 michael-chou359 force-pushed the mc/pydantic-ai-converter branch from 86e30bb to 739f1e5 Compare May 18, 2026 23:29
@michael-chou359 michael-chou359 force-pushed the mc/pydantic-ai-converter branch from 739f1e5 to bee0223 Compare May 18, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant