Skip to content

Commit 6c6e56f

Browse files
committed
Add missing AI SDK data chunk fields for reconciliation
1 parent acde3ec commit 6c6e56f

File tree

2 files changed

+53
-0
lines changed

2 files changed

+53
-0
lines changed

pydantic_ai_slim/pydantic_ai/ui/vercel_ai/response_types.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
ProviderMetadata = dict[str, dict[str, JSONValue]]
1717
"""Provider metadata."""
1818

19+
FinishReason = Literal['stop', 'length', 'content-filter', 'tool-calls', 'error', 'other', 'unknown'] | None
20+
"""Reason why the model finished generating."""
21+
1922

2023
class BaseChunk(CamelBaseModel, ABC):
2124
"""Abstract base class for response SSE events."""
@@ -145,6 +148,21 @@ class ToolOutputErrorChunk(BaseChunk):
145148
dynamic: bool | None = None
146149

147150

151+
class ToolApprovalRequestChunk(BaseChunk):
152+
"""Tool approval request chunk for human-in-the-loop approval."""
153+
154+
type: Literal['tool-approval-request'] = 'tool-approval-request'
155+
approval_id: str
156+
tool_call_id: str
157+
158+
159+
class ToolOutputDeniedChunk(BaseChunk):
160+
"""Tool output denied chunk when user denies tool execution."""
161+
162+
type: Literal['tool-output-denied'] = 'tool-output-denied'
163+
tool_call_id: str
164+
165+
148166
class SourceUrlChunk(BaseChunk):
149167
"""Source URL chunk."""
150168

@@ -178,7 +196,9 @@ class DataChunk(BaseChunk):
178196
"""Data chunk with dynamic type."""
179197

180198
type: Annotated[str, Field(pattern=r'^data-')]
199+
id: str | None = None
181200
data: Any
201+
transient: bool | None = None
182202

183203

184204
class StartStepChunk(BaseChunk):
@@ -205,6 +225,7 @@ class FinishChunk(BaseChunk):
205225
"""Finish chunk."""
206226

207227
type: Literal['finish'] = 'finish'
228+
finish_reason: FinishReason = None
208229
message_metadata: Any | None = None
209230

210231

tests/test_vercel_ai.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,38 @@ async def on_complete(run_result: AgentRunResult[Any]) -> AsyncIterator[BaseChun
16191619
)
16201620

16211621

1622+
async def test_data_chunk_with_id_and_transient():
1623+
"""Test DataChunk supports optional id and transient fields for AI SDK compatibility."""
1624+
agent = Agent(model=TestModel())
1625+
1626+
request = SubmitMessage(
1627+
id='foo',
1628+
messages=[
1629+
UIMessage(
1630+
id='bar',
1631+
role='user',
1632+
parts=[TextUIPart(text='Hello')],
1633+
),
1634+
],
1635+
)
1636+
1637+
async def on_complete(run_result: AgentRunResult[Any]) -> AsyncIterator[BaseChunk]:
1638+
# Yield a data chunk with id for reconciliation
1639+
yield DataChunk(type='data-task', id='task-123', data={'status': 'complete'})
1640+
# Yield a transient data chunk (not persisted to history)
1641+
yield DataChunk(type='data-progress', data={'percent': 100}, transient=True)
1642+
1643+
adapter = VercelAIAdapter(agent, request)
1644+
events = [
1645+
'[DONE]' if '[DONE]' in event else json.loads(event.removeprefix('data: '))
1646+
async for event in adapter.encode_stream(adapter.run_stream(on_complete=on_complete))
1647+
]
1648+
1649+
# Verify the data chunks are present in the events with correct fields
1650+
assert {'type': 'data-task', 'id': 'task-123', 'data': {'status': 'complete'}} in events
1651+
assert {'type': 'data-progress', 'data': {'percent': 100}, 'transient': True} in events
1652+
1653+
16221654
@pytest.mark.skipif(not starlette_import_successful, reason='Starlette is not installed')
16231655
async def test_adapter_dispatch_request():
16241656
agent = Agent(model=TestModel())

0 commit comments

Comments
 (0)