From f255989984df48bf438ae28d6b3025653e61092f Mon Sep 17 00:00:00 2001 From: Jack Herrington Date: Fri, 20 Feb 2026 15:54:23 -0800 Subject: [PATCH 1/3] refactor: change CustomEvent property from 'data' to 'value' for AG-UI compliance Co-Authored-By: Warp --- .changeset/wicked-humans-shop.md | 6 ++++++ packages/python/tanstack-ai/src/tanstack_ai/chat.py | 4 ++-- packages/python/tanstack-ai/src/tanstack_ai/types.py | 2 +- packages/typescript/ai/src/activities/chat/index.ts | 8 ++++---- .../ai/src/activities/chat/stream/processor.ts | 12 ++++++------ packages/typescript/ai/src/types.ts | 4 ++-- 6 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 .changeset/wicked-humans-shop.md diff --git a/.changeset/wicked-humans-shop.md b/.changeset/wicked-humans-shop.md new file mode 100644 index 000000000..182e55045 --- /dev/null +++ b/.changeset/wicked-humans-shop.md @@ -0,0 +1,6 @@ +--- +'@tanstack/ai-client': patch +'@tanstack/ai': patch +--- + +Refactor CustomEvent property from 'data' to 'value' for AG-UI compliance diff --git a/packages/python/tanstack-ai/src/tanstack_ai/chat.py b/packages/python/tanstack-ai/src/tanstack_ai/chat.py index 36d2ab86d..7cd044b85 100644 --- a/packages/python/tanstack-ai/src/tanstack_ai/chat.py +++ b/packages/python/tanstack-ai/src/tanstack_ai/chat.py @@ -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, @@ -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, diff --git a/packages/python/tanstack-ai/src/tanstack_ai/types.py b/packages/python/tanstack-ai/src/tanstack_ai/types.py index 7a853a103..05f33ec4d 100644 --- a/packages/python/tanstack-ai/src/tanstack_ai/types.py +++ b/packages/python/tanstack-ai/src/tanstack_ai/types.py @@ -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 diff --git a/packages/typescript/ai/src/activities/chat/index.ts b/packages/typescript/ai/src/activities/chat/index.ts index 3964bc1da..f7f4b7a66 100644 --- a/packages/typescript/ai/src/activities/chat/index.ts +++ b/packages/typescript/ai/src/activities/chat/index.ts @@ -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, @@ -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, @@ -1094,14 +1094,14 @@ class TextEngine< private createCustomEventChunk( eventName: string, - data: Record, + value: Record, ): CustomEvent { return { type: 'CUSTOM', timestamp: Date.now(), model: this.params.model, name: eventName, - data, + value, } } diff --git a/packages/typescript/ai/src/activities/chat/stream/processor.ts b/packages/typescript/ai/src/activities/chat/stream/processor.ts index a91a46d89..b48b66adc 100644 --- a/packages/typescript/ai/src/activities/chat/stream/processor.ts +++ b/packages/typescript/ai/src/activities/chat/stream/processor.ts @@ -814,8 +814,8 @@ export class StreamProcessor { chunk: Extract, ): 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 @@ -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 @@ -863,8 +863,8 @@ export class StreamProcessor { } // Forward all other custom events to the callback - this.events.onCustomEvent?.(chunk.name, chunk.data, { - toolCallId: (chunk.data as any)?.toolCallId, + this.events.onCustomEvent?.(chunk.name, chunk.value, { + toolCallId: (chunk.value as any)?.toolCallId, }) } diff --git a/packages/typescript/ai/src/types.ts b/packages/typescript/ai/src/types.ts index eb9c39ceb..00bca79fe 100644 --- a/packages/typescript/ai/src/types.ts +++ b/packages/typescript/ai/src/types.ts @@ -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 } /** From 1dfbd72c66c26cbd3c7e3e6f345a932f09fa5d89 Mon Sep 17 00:00:00 2001 From: Jack Herrington Date: Fri, 20 Feb 2026 16:50:57 -0800 Subject: [PATCH 2/3] fixing the oh so lovely tests --- .../ai-client/tests/chat-client.test.ts | 18 +++++++++--------- .../typescript/ai-client/tests/test-utils.ts | 6 +++--- .../ai/src/activities/chat/stream/processor.ts | 14 ++++++++------ .../ai/src/activities/chat/tools/tool-calls.ts | 6 +++--- packages/typescript/ai/src/types.ts | 4 ++-- packages/typescript/ai/tests/chat.test.ts | 16 ++++++++-------- .../ai/tests/custom-events-integration.test.ts | 10 +++++----- .../ai/tests/stream-processor.test.ts | 2 +- .../ai/tests/stream-to-response.test.ts | 12 ++++++------ 9 files changed, 45 insertions(+), 43 deletions(-) diff --git a/packages/typescript/ai-client/tests/chat-client.test.ts b/packages/typescript/ai-client/tests/chat-client.test.ts index 2976d4945..0af380960 100644 --- a/packages/typescript/ai-client/tests/chat-client.test.ts +++ b/packages/typescript/ai-client/tests/chat-client.test.ts @@ -906,10 +906,10 @@ describe('ChatClient', () => { describe('custom events', () => { it('should call onCustomEvent callback for arbitrary custom events', async () => { const chunks = createCustomEventChunks([ - { name: 'progress-update', data: { progress: 50, step: 'processing' } }, + { name: 'progress-update', value: { progress: 50, step: 'processing' } }, { name: 'tool-status', - data: { toolCallId: 'tc-1', status: 'running' }, + value: { toolCallId: 'tc-1', status: 'running' }, }, ]) const adapter = createMockConnectionAdapter({ chunks }) @@ -938,7 +938,7 @@ describe('ChatClient', () => { const chunks = createCustomEventChunks([ { name: 'external-api-call', - data: { + value: { toolCallId: 'tc-123', url: 'https://api.example.com', method: 'POST', @@ -996,7 +996,7 @@ describe('ChatClient', () => { it('should work when onCustomEvent is not provided', async () => { const chunks = createCustomEventChunks([ - { name: 'some-event', data: { info: 'test' } }, + { name: 'some-event', value: { info: 'test' } }, ]) const adapter = createMockConnectionAdapter({ chunks }) @@ -1008,7 +1008,7 @@ describe('ChatClient', () => { it('should allow updating onCustomEvent via updateOptions', async () => { const chunks = createCustomEventChunks([ - { name: 'test-event', data: { value: 42 } }, + { name: 'test-event', value: { value: 42 } }, ]) const adapter = createMockConnectionAdapter({ chunks }) @@ -1028,9 +1028,9 @@ describe('ChatClient', () => { it('should handle multiple different custom events in sequence', async () => { const chunks = createCustomEventChunks([ - { name: 'step-1', data: { stage: 'init' } }, - { name: 'step-2', data: { stage: 'process', toolCallId: 'tc-1' } }, - { name: 'step-3', data: { stage: 'complete' } }, + { name: 'step-1', value: { stage: 'init' } }, + { name: 'step-2', value: { stage: 'process', toolCallId: 'tc-1' } }, + { name: 'step-3', value: { stage: 'complete' } }, ]) const adapter = createMockConnectionAdapter({ chunks }) @@ -1070,7 +1070,7 @@ describe('ChatClient', () => { } const chunks = createCustomEventChunks([ - { name: 'complex-data-event', data: complexEventData }, + { name: 'complex-data-event', value: complexEventData }, ]) const adapter = createMockConnectionAdapter({ chunks }) diff --git a/packages/typescript/ai-client/tests/test-utils.ts b/packages/typescript/ai-client/tests/test-utils.ts index aedc59298..fa736d04b 100644 --- a/packages/typescript/ai-client/tests/test-utils.ts +++ b/packages/typescript/ai-client/tests/test-utils.ts @@ -146,7 +146,7 @@ export function createTextChunks( * Helper to create custom event chunks */ export function createCustomEventChunks( - events: Array<{ name: string; data?: unknown }>, + events: Array<{ name: string; value?: unknown }>, model: string = 'test', ): Array { const chunks: Array = [] @@ -157,7 +157,7 @@ export function createCustomEventChunks( model, timestamp: Date.now(), name: event.name, - data: event.data, + value: event.value, }) } @@ -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, diff --git a/packages/typescript/ai/src/activities/chat/stream/processor.ts b/packages/typescript/ai/src/activities/chat/stream/processor.ts index b48b66adc..f14152b9d 100644 --- a/packages/typescript/ai/src/activities/chat/stream/processor.ts +++ b/packages/typescript/ai/src/activities/chat/stream/processor.ts @@ -827,7 +827,6 @@ export class StreamProcessor { toolName, input, }) - return } @@ -858,14 +857,17 @@ export class StreamProcessor { input, approvalId: approval.id, }) - return } - // Forward all other custom events to the callback - this.events.onCustomEvent?.(chunk.name, chunk.value, { - toolCallId: (chunk.value as any)?.toolCallId, - }) + // Forward non-system custom events to onCustomEvent callback + if (this.events.onCustomEvent) { + const toolCallId = + chunk.value && typeof chunk.value === 'object' + ? (chunk.value as any).toolCallId + : undefined + this.events.onCustomEvent(chunk.name, chunk.value, { toolCallId }) + } } /** diff --git a/packages/typescript/ai/src/activities/chat/tools/tool-calls.ts b/packages/typescript/ai/src/activities/chat/tools/tool-calls.ts index 0c622b9ec..594274110 100644 --- a/packages/typescript/ai/src/activities/chat/tools/tool-calls.ts +++ b/packages/typescript/ai/src/activities/chat/tools/tool-calls.ts @@ -311,7 +311,7 @@ export async function* executeToolCalls( clientResults: Map = new Map(), createCustomEventChunk?: ( eventName: string, - data: Record, + value: Record, ) => CustomEvent, ): AsyncGenerator { const results: Array = [] @@ -376,11 +376,11 @@ export async function* executeToolCalls( const pendingEvents: Array = [] const context: ToolExecutionContext = { toolCallId: toolCall.id, - emitCustomEvent: (eventName: string, data: Record) => { + emitCustomEvent: (eventName: string, value: Record) => { if (createCustomEventChunk) { pendingEvents.push( createCustomEventChunk(eventName, { - ...data, + ...value, toolCallId: toolCall.id, }), ) diff --git a/packages/typescript/ai/src/types.ts b/packages/typescript/ai/src/types.ts index 00bca79fe..49e800fc5 100644 --- a/packages/typescript/ai/src/types.ts +++ b/packages/typescript/ai/src/types.ts @@ -356,7 +356,7 @@ export interface ToolExecutionContext { * Events are streamed to the client in real-time as AG-UI CUSTOM events. * * @param eventName - Name of the custom event - * @param data - Event payload data + * @param value - Event payload value * * @example * ```ts @@ -369,7 +369,7 @@ export interface ToolExecutionContext { * }) * ``` */ - emitCustomEvent: (eventName: string, data: Record) => void + emitCustomEvent: (eventName: string, value: Record) => void } /** diff --git a/packages/typescript/ai/tests/chat.test.ts b/packages/typescript/ai/tests/chat.test.ts index a5dfcb270..65bda0067 100644 --- a/packages/typescript/ai/tests/chat.test.ts +++ b/packages/typescript/ai/tests/chat.test.ts @@ -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' }) }) }) @@ -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 () => { diff --git a/packages/typescript/ai/tests/custom-events-integration.test.ts b/packages/typescript/ai/tests/custom-events-integration.test.ts index 1bee12fcd..9fe31fb6c 100644 --- a/packages/typescript/ai/tests/custom-events-integration.test.ts +++ b/packages/typescript/ai/tests/custom-events-integration.test.ts @@ -94,7 +94,7 @@ describe('Custom Events Integration', () => { processor.processChunk({ type: 'CUSTOM', name: eventName, - data: { ...data, toolCallId: 'tc-1' }, + value: { ...data, toolCallId: 'tc-1' }, timestamp: Date.now(), }) }, @@ -160,7 +160,7 @@ describe('Custom Events Integration', () => { processor.processChunk({ type: 'CUSTOM', name: 'system:status', - data: { status: 'ready', version: '1.0.0' }, + value: { status: 'ready', version: '1.0.0' }, timestamp: Date.now(), }) @@ -197,7 +197,7 @@ describe('Custom Events Integration', () => { processor.processChunk({ type: 'CUSTOM', name: 'tool-input-available', - data: { + value: { toolCallId: 'tc-1', toolName: 'testTool', input: { test: true }, @@ -209,7 +209,7 @@ describe('Custom Events Integration', () => { processor.processChunk({ type: 'CUSTOM', name: 'approval-requested', - data: { + value: { toolCallId: 'tc-2', toolName: 'dangerousTool', input: { action: 'delete' }, @@ -222,7 +222,7 @@ describe('Custom Events Integration', () => { processor.processChunk({ type: 'CUSTOM', name: 'user:custom-event', - data: { message: 'This should be forwarded' }, + value: { message: 'This should be forwarded' }, timestamp: Date.now(), }) diff --git a/packages/typescript/ai/tests/stream-processor.test.ts b/packages/typescript/ai/tests/stream-processor.test.ts index 3c2cddf88..f320517c6 100644 --- a/packages/typescript/ai/tests/stream-processor.test.ts +++ b/packages/typescript/ai/tests/stream-processor.test.ts @@ -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. */ diff --git a/packages/typescript/ai/tests/stream-to-response.test.ts b/packages/typescript/ai/tests/stream-to-response.test.ts index 06f864506..dc2dbe300 100644 --- a/packages/typescript/ai/tests/stream-to-response.test.ts +++ b/packages/typescript/ai/tests/stream-to-response.test.ts @@ -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' }, @@ -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' }, @@ -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 () => { @@ -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' }, From 70645985848c1e4eea3624a2824fc0267a2ea610 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 00:52:19 +0000 Subject: [PATCH 3/3] ci: apply automated fixes --- packages/typescript/ai-client/tests/chat-client.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/typescript/ai-client/tests/chat-client.test.ts b/packages/typescript/ai-client/tests/chat-client.test.ts index 0af380960..5b9bf956d 100644 --- a/packages/typescript/ai-client/tests/chat-client.test.ts +++ b/packages/typescript/ai-client/tests/chat-client.test.ts @@ -906,7 +906,10 @@ describe('ChatClient', () => { describe('custom events', () => { it('should call onCustomEvent callback for arbitrary custom events', async () => { const chunks = createCustomEventChunks([ - { name: 'progress-update', value: { progress: 50, step: 'processing' } }, + { + name: 'progress-update', + value: { progress: 50, step: 'processing' }, + }, { name: 'tool-status', value: { toolCallId: 'tc-1', status: 'running' },