Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .changeset/perky-sails-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
'@tanstack/tests-adapters': patch
'@tanstack/ai-client': patch
'@tanstack/ai': patch
---
Comment on lines +2 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Breaking API change incorrectly versioned as patch.

Renaming CustomEvent.data β†’ CustomEvent.value silently breaks any consumer that reads event.data (they get undefined with no error). This is a breaking change and must be versioned accordingly:

  • @tanstack/ai and @tanstack/ai-client: should be minor at minimum (or major if following strict SemVer). A patch bump gives consumers no semver signal that their code may break.
  • @tanstack/tests-adapters: patch is acceptable since it is a test/dev-only package.
πŸ”§ Suggested fix
 ---
-'@tanstack/tests-adapters': patch
-'@tanstack/ai-client': patch
-'@tanstack/ai': patch
+'@tanstack/tests-adapters': patch
+'@tanstack/ai-client': minor
+'@tanstack/ai': minor
 ---
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'@tanstack/tests-adapters': patch
'@tanstack/ai-client': patch
'@tanstack/ai': patch
---
'@tanstack/tests-adapters': patch
'@tanstack/ai-client': minor
'@tanstack/ai': minor
---
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/perky-sails-brush.md around lines 2 - 5, The changeset wrongly
marks a breaking rename (CustomEvent.data β†’ CustomEvent.value) as a patch;
update the .changeset so that '@tanstack/ai' and '@tanstack/ai-client' are
bumped to at least "minor" (or "major" if you follow strict SemVer) while
keeping '@tanstack/tests-adapters' as "patch", and while you’re here consider
adding a compatibility note or a temporary shim (e.g., map event.data to
event.value) in the code paths that emit CustomEvent so consumers reading
event.data won’t silently break; reference the symbols CustomEvent.data,
CustomEvent.value and the package names '@tanstack/ai' and '@tanstack/ai-client'
when making these edits.


Refactor CustomEvent property from 'data' to 'value' for AG-UI compliance

## What Changed

The `CustomEvent` interface and class now use a `value` property instead of `data` to align with the AG-UI specification for custom events.

### TypeScript

```typescript
// Before
interface CustomEvent {
type: 'CUSTOM'
name: string
data?: unknown
}

// After
interface CustomEvent {
type: 'CUSTOM'
name: string
value?: unknown
}
```

### Python

```python
# Before
class CustomEvent:
def __init__(self, name: str, data=None):
self.data = data

# After
class CustomEvent:
def __init__(self, name: str, value=None):
self.value = value
```

## Migration Guide

Update any code that accesses the `data` property on CustomEvent objects:

```typescript
// Before
if (chunk.type === 'CUSTOM' && chunk.data) {
console.log(chunk.data)
}

// After
if (chunk.type === 'CUSTOM' && chunk.value) {
console.log(chunk.value)
}
```

This affects:

- Custom event handlers that access event data
- Test utilities that create or verify CustomEvent objects
- Stream processing code that handles CUSTOM event types
6 changes: 3 additions & 3 deletions docs/reference/interfaces/CustomEvent.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ Custom event for extensibility.

## Properties

### data?
### value?

```ts
optional data: unknown;
optional value: unknown;
```

Defined in: [types.ts:898](https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/types.ts#L898)

Custom event data
Custom event value

***

Expand Down
4 changes: 2 additions & 2 deletions packages/python/tanstack-ai/src/tanstack_ai/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ async def _emit_approval_requests(
"timestamp": int(time.time() * 1000),
"model": finish_event.get("model"),
"name": "approval-requested",
"data": {
"value": {
"toolCallId": approval.tool_call_id,
"toolName": approval.tool_name,
"input": approval.input,
Expand All @@ -410,7 +410,7 @@ async def _emit_client_tool_inputs(
"timestamp": int(time.time() * 1000),
"model": finish_event.get("model"),
"name": "tool-input-available",
"data": {
"value": {
"toolCallId": client_tool.tool_call_id,
"toolName": client_tool.tool_name,
"input": client_tool.input,
Expand Down
2 changes: 1 addition & 1 deletion packages/python/tanstack-ai/src/tanstack_ai/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ class CustomEvent(TypedDict, total=False):
timestamp: int
model: Optional[str]
name: str
data: Optional[Any]
value: Optional[Any]


# Union of all AG-UI events
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript/ai-client/tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export function createToolCallChunks(
model,
timestamp: Date.now(),
name: 'tool-input-available',
data: {
value: {
toolCallId: toolCall.id,
toolName: toolCall.name,
input: parsedInput,
Expand Down
4 changes: 2 additions & 2 deletions packages/typescript/ai/src/activities/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ class TextEngine<
timestamp: Date.now(),
model: finishEvent.model,
name: 'approval-requested',
data: {
value: {
toolCallId: approval.toolCallId,
toolName: approval.toolName,
input: approval.input,
Expand Down Expand Up @@ -879,7 +879,7 @@ class TextEngine<
timestamp: Date.now(),
model: finishEvent.model,
name: 'tool-input-available',
data: {
value: {
toolCallId: clientTool.toolCallId,
toolName: clientTool.toolName,
input: clientTool.input,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -814,8 +814,8 @@ export class StreamProcessor {
chunk: Extract<StreamChunk, { type: 'CUSTOM' }>,
): void {
// Handle client tool input availability - trigger client-side execution
if (chunk.name === 'tool-input-available' && chunk.data) {
const { toolCallId, toolName, input } = chunk.data as {
if (chunk.name === 'tool-input-available' && chunk.value) {
const { toolCallId, toolName, input } = chunk.value as {
toolCallId: string
toolName: string
input: any
Expand All @@ -832,8 +832,8 @@ export class StreamProcessor {
}

// Handle approval requests
if (chunk.name === 'approval-requested' && chunk.data) {
const { toolCallId, toolName, input, approval } = chunk.data as {
if (chunk.name === 'approval-requested' && chunk.value) {
const { toolCallId, toolName, input, approval } = chunk.value as {
toolCallId: string
toolName: string
input: any
Expand Down
4 changes: 2 additions & 2 deletions packages/typescript/ai/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,8 +922,8 @@ export interface CustomEvent extends BaseAGUIEvent {
type: 'CUSTOM'
/** Custom event name */
name: string
/** Custom event data */
data?: unknown
/** Custom event value */
value?: unknown
}

/**
Expand Down
16 changes: 8 additions & 8 deletions packages/typescript/ai/tests/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,10 +476,10 @@ describe('chat()', () => {
)
expect(customChunks).toHaveLength(1)

const data = (customChunks[0] as any).data
expect(data.toolCallId).toBe('call_1')
expect(data.toolName).toBe('clientSearch')
expect(data.input).toEqual({ query: 'test' })
const value = (customChunks[0] as any).value
expect(value.toolCallId).toBe('call_1')
expect(value.toolName).toBe('clientSearch')
expect(value.input).toEqual({ query: 'test' })
})
})

Expand Down Expand Up @@ -515,10 +515,10 @@ describe('chat()', () => {
)
expect(approvalChunks).toHaveLength(1)

const data = (approvalChunks[0] as any).data
expect(data.toolCallId).toBe('call_1')
expect(data.toolName).toBe('dangerousTool')
expect(data.approval.needsApproval).toBe(true)
const value = (approvalChunks[0] as any).value
expect(value.toolCallId).toBe('call_1')
expect(value.toolName).toBe('dangerousTool')
expect(value.approval.needsApproval).toBe(true)
})

it('should yield CUSTOM approval-requested for client tools with needsApproval', async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/typescript/ai/tests/stream-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ev = {
chunk('RUN_ERROR', { runId, error: { message } }),
stepFinished: (delta: string, stepId = 'step-1') =>
chunk('STEP_FINISHED', { stepId, delta }),
custom: (name: string, data?: unknown) => chunk('CUSTOM', { name, data }),
custom: (name: string, value?: unknown) => chunk('CUSTOM', { name, value }),
}

/** Events object with vi.fn() mocks for assertions. */
Expand Down Expand Up @@ -952,7 +952,7 @@ describe('StreamProcessor', () => {
})
})

it('should not fire onToolCall for CUSTOM events with no data', () => {
it('should not fire onToolCall for CUSTOM events with no value', () => {
const events = spyEvents()
const processor = new StreamProcessor({ events })
processor.prepareAssistantMessage()
Expand Down Expand Up @@ -1068,7 +1068,7 @@ describe('StreamProcessor', () => {
expect(toolCallPart.approval?.approved).toBe(false)
})

it('should not fire onApprovalRequest for approval-requested without data', () => {
it('should not fire onApprovalRequest for approval-requested without value', () => {
const events = spyEvents()
const processor = new StreamProcessor({ events })
processor.prepareAssistantMessage()
Expand Down
12 changes: 6 additions & 6 deletions packages/typescript/ai/tests/stream-to-response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ describe('SSE Round-Trip (Encode β†’ Decode)', () => {
model: 'test',
timestamp: Date.now(),
name: 'tool-input-available',
data: {
value: {
toolCallId: 'tc-1',
toolName: 'get_weather',
input: { city: 'NYC', units: 'fahrenheit' },
Expand All @@ -673,7 +673,7 @@ describe('SSE Round-Trip (Encode β†’ Decode)', () => {
model: 'test',
timestamp: Date.now(),
name: 'approval-requested',
data: {
value: {
toolCallId: 'tc-2',
toolName: 'delete_file',
input: { path: '/tmp/file.txt' },
Expand All @@ -690,13 +690,13 @@ describe('SSE Round-Trip (Encode β†’ Decode)', () => {
// Verify tool-input-available
expect(parsedChunks[0]?.type).toBe('CUSTOM')
expect((parsedChunks[0] as any)?.name).toBe('tool-input-available')
expect((parsedChunks[0] as any)?.data?.toolCallId).toBe('tc-1')
expect((parsedChunks[0] as any)?.data?.input?.city).toBe('NYC')
expect((parsedChunks[0] as any)?.value?.toolCallId).toBe('tc-1')
expect((parsedChunks[0] as any)?.value?.input?.city).toBe('NYC')

// Verify approval-requested
expect(parsedChunks[1]?.type).toBe('CUSTOM')
expect((parsedChunks[1] as any)?.name).toBe('approval-requested')
expect((parsedChunks[1] as any)?.data?.approval?.id).toBe('approval-1')
expect((parsedChunks[1] as any)?.value?.approval?.id).toBe('approval-1')
})

it('should preserve TEXT_MESSAGE_START/END events', async () => {
Expand Down Expand Up @@ -787,7 +787,7 @@ describe('SSE Round-Trip (Encode β†’ Decode)', () => {
model: 'test',
timestamp: Date.now(),
name: 'tool-input-available',
data: {
value: {
toolCallId: 'tc-1',
toolName: 'search',
input: { query: 'test' },
Expand Down
6 changes: 3 additions & 3 deletions packages/typescript/smoke-tests/adapters/src/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,11 @@ export async function captureStream(opts: {
// AG-UI CUSTOM events (approval requests, tool inputs, etc.)
else if (chunk.type === 'CUSTOM') {
chunkData.name = chunk.name
chunkData.data = chunk.data
chunkData.value = chunk.value

// Handle approval-requested CUSTOM events
if (chunk.name === 'approval-requested' && chunk.data) {
const data = chunk.data as {
if (chunk.name === 'approval-requested' && chunk.value) {
const data = chunk.value as {
toolCallId: string
toolName: string
input: any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ describe('Approval Flow Tests', () => {
expect(approvalChunks.length).toBe(1)

const approvalChunk = approvalChunks[0] as any
expect(approvalChunk.data.toolName).toBe('delete_file')
expect(approvalChunk.data.input).toEqual({ path: '/tmp/important.txt' })
expect(approvalChunk.data.approval.needsApproval).toBe(true)
expect(approvalChunk.value.toolName).toBe('delete_file')
expect(approvalChunk.value.input).toEqual({ path: '/tmp/important.txt' })
expect(approvalChunk.value.approval.needsApproval).toBe(true)

// Tool should NOT be executed yet (waiting for approval)
expect(executeFn).not.toHaveBeenCalled()
Expand Down Expand Up @@ -373,7 +373,7 @@ describe('Approval Flow Tests', () => {
(c: any) => c.type === 'CUSTOM' && c.name === 'approval-requested',
)
expect(approvalChunks.length).toBe(1)
expect((approvalChunks[0] as any).data.toolName).toBe('delete_item')
expect((approvalChunks[0] as any).value.toolName).toBe('delete_item')

// Check tool should have been executed (verify via mock call)
expect(checkExecute).toHaveBeenCalledWith({ id: '123' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ describe('Client Tool Tests', () => {
expect(inputAvailableChunks.length).toBe(1)

const inputChunk = inputAvailableChunks[0] as any
expect(inputChunk.data.toolName).toBe('show_notification')
expect(inputChunk.data.input).toEqual({
expect(inputChunk.value.toolName).toBe('show_notification')
expect(inputChunk.value.input).toEqual({
message: 'Hello World',
type: 'info',
})
Expand Down Expand Up @@ -276,7 +276,7 @@ describe('Client Tool Tests', () => {

// At least one should be for tool_b
const toolBInputs = inputChunks.filter(
(c: any) => c.data?.toolName === 'client_tool_b',
(c: any) => c.value?.toolName === 'client_tool_b',
)
expect(toolBInputs.length).toBe(1)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ describe('Tool Sequence Tests', () => {
(c: any) => c.type === 'CUSTOM' && c.name === 'tool-input-available',
)
expect(inputChunks.length).toBe(1)
expect((inputChunks[0] as any).data.toolName).toBe('client_confirm')
expect((inputChunks[0] as any).value.toolName).toBe('client_confirm')
})
})

Expand Down
Loading