Skip to content

Added Pydantic AI sync, async, temporal integration#359

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

Added Pydantic AI sync, async, temporal integration#359
michael-chou359 wants to merge 3 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 for Agentex across three deployment modes: synchronous HTTP-yield (sync ACP), asynchronous Redis-publish (async ACP), and Temporal-durable async. It also ships a new CoalescingBuffer-backed StreamingTaskMessageContext for time-and-size-windowed delta merging, and three end-to-end tutorial examples.

  • _pydantic_ai_sync.py converts Pydantic AI AgentStreamEvents into StreamTaskMessage* events for direct HTTP streaming; correctly handles text, tool-call argument deltas, and tool results — but ThinkingPart start events emit TextContent instead of ReasoningContent (flagged in a prior review), and the test suite does not assert the start-content type so the gap is not caught.
  • _pydantic_ai_async.py pushes events to Redis via adk.streaming contexts and adk.messages.create; serializes non-string tool-result content with bare str() rather than the richer _tool_return_content helper already present in the sync module, producing Python repr strings instead of structured data.
  • streaming.py introduces CoalescingBuffer, DeltaAccumulator, and StreamingTaskMessageContext with correct buffer-drain-before-done sequencing and finally-block cleanup.

Confidence Score: 4/5

The sync converter contains a type mismatch in the ThinkingPart start event that was flagged in the prior review and remains unresolved; all other new logic is sound.

The sync converter emits TextContent in the StreamTaskMessageStart for ThinkingPart, then immediately follows with ReasoningContentDelta deltas. Any consumer that uses the declared content type to select a renderer will treat thinking output as a plain-text bubble. The async module and Temporal integration are clean; the async tests are thorough; the streaming service buffer mechanics are correct.

src/agentex/lib/adk/_modules/_pydantic_ai_sync.py — the ThinkingPart branch of PartStartEvent (around line 159) needs the content type changed from TextContent to ReasoningContent, mirroring the async helper. tests/lib/adk/test_pydantic_ai_sync.pyTestThinkingStreaming should add an isinstance(..., ReasoningContent) assertion on the start event to lock this in.

Important Files Changed

Filename Overview
src/agentex/lib/adk/_modules/_pydantic_ai_sync.py New sync converter for Pydantic AI → Agentex streaming events. Contains the pre-existing P1 bug where ThinkingPart start events emit TextContent instead of ReasoningContent, causing a type mismatch with the subsequent ReasoningContentDelta deltas (flagged in prior review).
src/agentex/lib/adk/_modules/_pydantic_ai_async.py New async helper that publishes Pydantic AI events to Redis via streaming contexts. Correctly uses ReasoningContent for ThinkingPart and handles cleanup in finally. Non-string tool result content is serialized with str() rather than the richer _tool_return_content logic from the sync helper.
src/agentex/lib/core/services/adk/streaming.py Adds CoalescingBuffer (50ms/128-char window), DeltaAccumulator, and StreamingTaskMessageContext. The buffer correctly drains before sending DONE. The stream_update path for StreamTaskMessageDone would double-publish the done event, but this path is not exercised by any current caller.
tests/lib/adk/test_pydantic_ai_sync.py Good coverage of text, tool-call, multi-step, and edge-case scenarios. TestThinkingStreaming is missing an assertion on the start event's content type, leaving the existing TextContent/ReasoningContent mismatch undetected by the test suite.
tests/lib/adk/test_pydantic_ai_async.py Thorough async tests with in-memory fakes for streaming and messages modules. Correctly verifies context lifecycle, cleanup on exception, and all event types including reasoning.
examples/tutorials/10_async/10_temporal/110_pydantic_ai/project/workflow.py Temporal workflow for Pydantic AI agent; correctly delegates to temporal_agent.run() and closes via complete_task_signal. wait_condition(timeout=None) is intentional for a long-running conversational workflow.

Sequence Diagram

sequenceDiagram
    participant PA as Pydantic AI
    participant SC as Sync Converter
    participant AC as Async Helper
    participant ACP as FastACP (HTTP yield)
    participant Redis as Redis Stream
    participant Agentex as Agentex Server

    note over SC,AC: Two parallel paths for the same Pydantic AI events

    rect rgb(220, 240, 255)
        note over PA,ACP: Sync ACP path
        PA->>SC: PartStartEvent(TextPart)
        SC->>ACP: StreamTaskMessageStart(TextContent)
        PA->>SC: PartDeltaEvent(TextPartDelta)
        SC->>ACP: StreamTaskMessageDelta(TextDelta)
        PA->>SC: PartStartEvent(ToolCallPart)
        SC->>ACP: StreamTaskMessageStart(ToolRequestContent)
        PA->>SC: PartDeltaEvent(ToolCallPartDelta)
        SC->>ACP: StreamTaskMessageDelta(ToolRequestDelta)
        PA->>SC: FunctionToolResultEvent
        SC->>ACP: StreamTaskMessageFull(ToolResponseContent)
        PA->>SC: PartEndEvent
        SC->>ACP: StreamTaskMessageDone
    end

    rect rgb(240, 255, 220)
        note over PA,Agentex: Async ACP path (Redis + CoalescingBuffer)
        PA->>AC: PartStartEvent(TextPart)
        AC->>Redis: StreamingContext open → START
        PA->>AC: PartDeltaEvent(TextPartDelta)
        AC->>Redis: CoalescingBuffer.add(TextDelta)
        PA->>AC: PartEndEvent(TextPart)
        AC->>Redis: buffer.close() → flush → DONE
        PA->>AC: PartStartEvent(ToolCallPart)
        PA->>AC: PartEndEvent(ToolCallPart)
        AC->>Agentex: adk.messages.create(ToolRequestContent)
        PA->>AC: FunctionToolResultEvent
        AC->>Agentex: adk.messages.create(ToolResponseContent)
    end
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 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
tests/lib/adk/test_pydantic_ai_sync.py:110-147
**ThinkingPart start-content type not asserted in any test**

Every `TestThinkingStreaming` test checks that `out[0]` is a `StreamTaskMessageStart` but never asserts `isinstance(out[0].content, ReasoningContent)`. The existing bug — where the sync converter emits `TextContent` instead of `ReasoningContent` for the start event — is therefore invisible to the test suite. A single `assert isinstance(out[0].content, ReasoningContent)` line in `test_thinking_emits_reasoning_deltas` would have caught this immediately and would prevent regressions.

### Issue 2 of 2
src/agentex/lib/adk/_modules/_pydantic_ai_async.py:208-223
**`FunctionToolResultEvent` stringifies structured content; sync helper preserves it**

The async helper converts non-string tool results with plain `str(content)`, producing a Python `repr` string (e.g., `"{'temp': 72, 'sky': 'clear'}"`) that is neither valid JSON nor idiomatic. The sync module's `_tool_return_content` helper already handles dicts, lists, primitives, and Pydantic models correctly and is importable from the same package — using it here would make both paths consistent and keep dict results in a parseable form.

```suggestion
                from agentex.lib.adk._modules._pydantic_ai_sync import _tool_return_content

                await adk.messages.create(
                    task_id=task_id,
                    content=ToolResponseContent(
                        tool_call_id=tool_call_id,
                        name=tool_name,
                        content=_tool_return_content(result),
                        author="agent",
                    ),
                )
```

Reviews (3): Last reviewed commit: "fix lint errors" | 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