From 0120ea8bb3f6855f7148296866ef7c1b76bc12bf Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Mon, 17 Aug 2026 22:53:52 +0800 Subject: [PATCH 1/3] fix: preserve fallback structured output timestamp ordering --- .changeset/gentle-dots-fallback-timestamps.md | 5 +++ packages/ai/src/activities/chat/index.ts | 2 + .../chat-structured-output-stream.test.ts | 38 +++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 .changeset/gentle-dots-fallback-timestamps.md diff --git a/.changeset/gentle-dots-fallback-timestamps.md b/.changeset/gentle-dots-fallback-timestamps.md new file mode 100644 index 0000000000..43420ced70 --- /dev/null +++ b/.changeset/gentle-dots-fallback-timestamps.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai': patch +--- + +Fix fallback structured-output lifecycle timestamps being emitted before the provider request settles. diff --git a/packages/ai/src/activities/chat/index.ts b/packages/ai/src/activities/chat/index.ts index 9bf2761aa0..0a1d1e2402 100644 --- a/packages/ai/src/activities/chat/index.ts +++ b/packages/ai/src/activities/chat/index.ts @@ -5022,6 +5022,8 @@ async function* fallbackStructuredOutputStream( return } + const completedAt = Date.now() + yield { type: EventType.TEXT_MESSAGE_START, messageId, diff --git a/packages/ai/tests/chat-structured-output-stream.test.ts b/packages/ai/tests/chat-structured-output-stream.test.ts index 4e2012844e..0e501e0d68 100644 --- a/packages/ai/tests/chat-structured-output-stream.test.ts +++ b/packages/ai/tests/chat-structured-output-stream.test.ts @@ -471,6 +471,44 @@ describe('chat({ outputSchema, stream: true })', () => { expect(finished).toBeDefined() expect('usage' in finished!).toBe(false) }) + + it('keeps synthesized lifecycle timestamps ordered after a delayed provider result', async () => { + const adapter = makeAdapter({ + structuredOutput: async () => { + await new Promise((resolve) => setTimeout(resolve, 5)) + return { data: validPerson, rawText: JSON.stringify(validPerson) } + }, + }) + + const stream = chat({ + adapter, + messages: [{ role: 'user', content: 'extract' }], + outputSchema: PersonSchema, + stream: true, + }) + + const chunks = await collectChunks(stream) + const start = chunks.find( + (c) => + c.type === EventType.CUSTOM && + (c as { name?: string }).name === 'structured-output.start', + ) + const content = chunks.find((c) => c.type === EventType.TEXT_MESSAGE_CONTENT) + const complete = chunks.find( + (c) => + c.type === EventType.CUSTOM && + (c as { name?: string }).name === 'structured-output.complete', + ) + const finished = chunks.find((c) => c.type === EventType.RUN_FINISHED) + + expect(start).toBeDefined() + expect(content).toBeDefined() + expect(complete).toBeDefined() + expect(finished).toBeDefined() + expect(start!.timestamp).toBeLessThanOrEqual(content!.timestamp) + expect(content!.timestamp).toBeLessThanOrEqual(complete!.timestamp) + expect(complete!.timestamp).toBeLessThanOrEqual(finished!.timestamp) + }) }) describe('lifecycle ordering', () => { From 9a17ee57ba452fe275cd3442027ab4160e44dd4d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:00:20 +0000 Subject: [PATCH 2/3] ci: apply automated fixes --- packages/ai/tests/chat-structured-output-stream.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ai/tests/chat-structured-output-stream.test.ts b/packages/ai/tests/chat-structured-output-stream.test.ts index 0e501e0d68..7a48856c7a 100644 --- a/packages/ai/tests/chat-structured-output-stream.test.ts +++ b/packages/ai/tests/chat-structured-output-stream.test.ts @@ -493,7 +493,9 @@ describe('chat({ outputSchema, stream: true })', () => { c.type === EventType.CUSTOM && (c as { name?: string }).name === 'structured-output.start', ) - const content = chunks.find((c) => c.type === EventType.TEXT_MESSAGE_CONTENT) + const content = chunks.find( + (c) => c.type === EventType.TEXT_MESSAGE_CONTENT, + ) const complete = chunks.find( (c) => c.type === EventType.CUSTOM && From ad01e9bee2d9d9c4a4feade49edd0fe8a0cdd8a7 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Wed, 19 Aug 2026 12:10:18 +0800 Subject: [PATCH 3/3] test(ai): cover fallback structured output lifecycle ordering --- packages/ai/src/activities/chat/index.ts | 2 - .../chat-structured-output-stream.test.ts | 45 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/ai/src/activities/chat/index.ts b/packages/ai/src/activities/chat/index.ts index 0a1d1e2402..9bf2761aa0 100644 --- a/packages/ai/src/activities/chat/index.ts +++ b/packages/ai/src/activities/chat/index.ts @@ -5022,8 +5022,6 @@ async function* fallbackStructuredOutputStream( return } - const completedAt = Date.now() - yield { type: EventType.TEXT_MESSAGE_START, messageId, diff --git a/packages/ai/tests/chat-structured-output-stream.test.ts b/packages/ai/tests/chat-structured-output-stream.test.ts index 7a48856c7a..43f02b15b5 100644 --- a/packages/ai/tests/chat-structured-output-stream.test.ts +++ b/packages/ai/tests/chat-structured-output-stream.test.ts @@ -488,11 +488,15 @@ describe('chat({ outputSchema, stream: true })', () => { }) const chunks = await collectChunks(stream) + const runStarted = chunks.find((c) => c.type === EventType.RUN_STARTED) const start = chunks.find( (c) => c.type === EventType.CUSTOM && (c as { name?: string }).name === 'structured-output.start', ) + const textStart = chunks.find( + (c) => c.type === EventType.TEXT_MESSAGE_START, + ) const content = chunks.find( (c) => c.type === EventType.TEXT_MESSAGE_CONTENT, ) @@ -503,13 +507,48 @@ describe('chat({ outputSchema, stream: true })', () => { ) const finished = chunks.find((c) => c.type === EventType.RUN_FINISHED) + expect(runStarted).toBeDefined() expect(start).toBeDefined() + expect(textStart).toBeDefined() expect(content).toBeDefined() expect(complete).toBeDefined() expect(finished).toBeDefined() - expect(start!.timestamp).toBeLessThanOrEqual(content!.timestamp) - expect(content!.timestamp).toBeLessThanOrEqual(complete!.timestamp) - expect(complete!.timestamp).toBeLessThanOrEqual(finished!.timestamp) + expect(runStarted!.timestamp!).toBeLessThanOrEqual(start!.timestamp!) + expect(start!.timestamp!).toBeLessThanOrEqual(textStart!.timestamp!) + expect(textStart!.timestamp!).toBeLessThanOrEqual(content!.timestamp!) + expect(content!.timestamp!).toBeLessThanOrEqual(complete!.timestamp!) + expect(complete!.timestamp!).toBeLessThanOrEqual(finished!.timestamp!) + }) + + it('keeps synthesized error lifecycle timestamps ordered after a delayed provider rejection', async () => { + const adapter = makeAdapter({ + structuredOutput: async () => { + await new Promise((resolve) => setTimeout(resolve, 5)) + throw new Error('provider rejected the request') + }, + }) + + const stream = chat({ + adapter, + messages: [{ role: 'user', content: 'extract' }], + outputSchema: PersonSchema, + stream: true, + }) + + const chunks = await collectChunks(stream) + const runStarted = chunks.find((c) => c.type === EventType.RUN_STARTED) + const start = chunks.find( + (c) => + c.type === EventType.CUSTOM && + (c as { name?: string }).name === 'structured-output.start', + ) + const error = chunks.find((c) => c.type === EventType.RUN_ERROR) + + expect(runStarted).toBeDefined() + expect(start).toBeDefined() + expect(error).toBeDefined() + expect(runStarted!.timestamp!).toBeLessThanOrEqual(start!.timestamp!) + expect(start!.timestamp!).toBeLessThanOrEqual(error!.timestamp!) }) })