Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/wicked-humans-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/ai-client': patch
'@tanstack/ai': patch
---

Refactor CustomEvent property from 'data' to 'value' for AG-UI compliance
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
21 changes: 12 additions & 9 deletions packages/typescript/ai-client/tests/chat-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,10 +906,13 @@ 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 })
Expand Down Expand Up @@ -938,7 +941,7 @@ describe('ChatClient', () => {
const chunks = createCustomEventChunks([
{
name: 'external-api-call',
data: {
value: {
toolCallId: 'tc-123',
url: 'https://api.example.com',
method: 'POST',
Expand Down Expand Up @@ -996,7 +999,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 })

Expand All @@ -1008,7 +1011,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 })

Expand All @@ -1028,9 +1031,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 })

Expand Down Expand Up @@ -1070,7 +1073,7 @@ describe('ChatClient', () => {
}

const chunks = createCustomEventChunks([
{ name: 'complex-data-event', data: complexEventData },
{ name: 'complex-data-event', value: complexEventData },
])
const adapter = createMockConnectionAdapter({ chunks })

Expand Down
6 changes: 3 additions & 3 deletions packages/typescript/ai-client/tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StreamChunk> {
const chunks: Array<StreamChunk> = []
Expand All @@ -157,7 +157,7 @@ export function createCustomEventChunks(
model,
timestamp: Date.now(),
name: event.name,
data: event.data,
value: event.value,
})
}

Expand Down 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
8 changes: 4 additions & 4 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 Expand Up @@ -1094,14 +1094,14 @@ class TextEngine<

private createCustomEventChunk(
eventName: string,
data: Record<string, any>,
value: Record<string, any>,
): CustomEvent {
return {
type: 'CUSTOM',
timestamp: Date.now(),
model: this.params.model,
name: eventName,
data,
value,
}
}

Expand Down
22 changes: 12 additions & 10 deletions packages/typescript/ai/src/activities/chat/stream/processor.ts
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 @@ -827,13 +827,12 @@ export class StreamProcessor {
toolName,
input,
})

return
}

// 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 All @@ -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.data, {
toolCallId: (chunk.data 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 })
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ export async function* executeToolCalls(
clientResults: Map<string, any> = new Map(),
createCustomEventChunk?: (
eventName: string,
data: Record<string, any>,
value: Record<string, any>,
) => CustomEvent,
): AsyncGenerator<CustomEvent, ExecuteToolCallsResult, void> {
const results: Array<ToolResult> = []
Expand Down Expand Up @@ -376,11 +376,11 @@ export async function* executeToolCalls(
const pendingEvents: Array<CustomEvent> = []
const context: ToolExecutionContext = {
toolCallId: toolCall.id,
emitCustomEvent: (eventName: string, data: Record<string, any>) => {
emitCustomEvent: (eventName: string, value: Record<string, any>) => {
if (createCustomEventChunk) {
pendingEvents.push(
createCustomEventChunk(eventName, {
...data,
...value,
toolCallId: toolCall.id,
}),
)
Expand Down
8 changes: 4 additions & 4 deletions packages/typescript/ai/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -369,7 +369,7 @@ export interface ToolExecutionContext {
* })
* ```
*/
emitCustomEvent: (eventName: string, data: Record<string, any>) => void
emitCustomEvent: (eventName: string, value: Record<string, any>) => void
}

/**
Expand Down 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
10 changes: 5 additions & 5 deletions packages/typescript/ai/tests/custom-events-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
})
},
Expand Down Expand Up @@ -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(),
})

Expand Down Expand Up @@ -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 },
Expand All @@ -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' },
Expand All @@ -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(),
})

Expand Down
2 changes: 1 addition & 1 deletion 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
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
Loading