diff --git a/.changeset/server-tool-results-complete.md b/.changeset/server-tool-results-complete.md new file mode 100644 index 000000000..0b97fbd43 --- /dev/null +++ b/.changeset/server-tool-results-complete.md @@ -0,0 +1,7 @@ +--- +'@tanstack/ai': patch +'@tanstack/ai-client': patch +'@tanstack/ai-event-client': patch +--- + +Populate server-executed tool results on the matching `tool-call` part and mark successful tool calls as `complete`. diff --git a/examples/ts-react-chat/src/routeTree.gen.ts b/examples/ts-react-chat/src/routeTree.gen.ts index 45a3107ea..d0966d88a 100644 --- a/examples/ts-react-chat/src/routeTree.gen.ts +++ b/examples/ts-react-chat/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as RealtimeRouteImport } from './routes/realtime' +import { Route as Issue176ToolResultRouteImport } from './routes/issue-176-tool-result' import { Route as ImageGenRouteImport } from './routes/image-gen' import { Route as IndexRouteImport } from './routes/index' import { Route as GenerationsVideoRouteImport } from './routes/generations.video' @@ -38,6 +39,11 @@ const RealtimeRoute = RealtimeRouteImport.update({ path: '/realtime', getParentRoute: () => rootRouteImport, } as any) +const Issue176ToolResultRoute = Issue176ToolResultRouteImport.update({ + id: '/issue-176-tool-result', + path: '/issue-176-tool-result', + getParentRoute: () => rootRouteImport, +} as any) const ImageGenRoute = ImageGenRouteImport.update({ id: '/image-gen', path: '/image-gen', @@ -155,6 +161,7 @@ const ApiGenerateAudioRoute = ApiGenerateAudioRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/image-gen': typeof ImageGenRoute + '/issue-176-tool-result': typeof Issue176ToolResultRoute '/realtime': typeof RealtimeRoute '/api/image-gen': typeof ApiImageGenRoute '/api/structured-chat': typeof ApiStructuredChatRoute @@ -180,6 +187,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/image-gen': typeof ImageGenRoute + '/issue-176-tool-result': typeof Issue176ToolResultRoute '/realtime': typeof RealtimeRoute '/api/image-gen': typeof ApiImageGenRoute '/api/structured-chat': typeof ApiStructuredChatRoute @@ -206,6 +214,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/image-gen': typeof ImageGenRoute + '/issue-176-tool-result': typeof Issue176ToolResultRoute '/realtime': typeof RealtimeRoute '/api/image-gen': typeof ApiImageGenRoute '/api/structured-chat': typeof ApiStructuredChatRoute @@ -233,6 +242,7 @@ export interface FileRouteTypes { fullPaths: | '/' | '/image-gen' + | '/issue-176-tool-result' | '/realtime' | '/api/image-gen' | '/api/structured-chat' @@ -258,6 +268,7 @@ export interface FileRouteTypes { to: | '/' | '/image-gen' + | '/issue-176-tool-result' | '/realtime' | '/api/image-gen' | '/api/structured-chat' @@ -283,6 +294,7 @@ export interface FileRouteTypes { | '__root__' | '/' | '/image-gen' + | '/issue-176-tool-result' | '/realtime' | '/api/image-gen' | '/api/structured-chat' @@ -309,6 +321,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute ImageGenRoute: typeof ImageGenRoute + Issue176ToolResultRoute: typeof Issue176ToolResultRoute RealtimeRoute: typeof RealtimeRoute ApiImageGenRoute: typeof ApiImageGenRoute ApiStructuredChatRoute: typeof ApiStructuredChatRoute @@ -341,6 +354,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof RealtimeRouteImport parentRoute: typeof rootRouteImport } + '/issue-176-tool-result': { + id: '/issue-176-tool-result' + path: '/issue-176-tool-result' + fullPath: '/issue-176-tool-result' + preLoaderRoute: typeof Issue176ToolResultRouteImport + parentRoute: typeof rootRouteImport + } '/image-gen': { id: '/image-gen' path: '/image-gen' @@ -501,6 +521,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ImageGenRoute: ImageGenRoute, + Issue176ToolResultRoute: Issue176ToolResultRoute, RealtimeRoute: RealtimeRoute, ApiImageGenRoute: ApiImageGenRoute, ApiStructuredChatRoute: ApiStructuredChatRoute, diff --git a/examples/ts-react-chat/src/routes/issue-176-tool-result.tsx b/examples/ts-react-chat/src/routes/issue-176-tool-result.tsx new file mode 100644 index 000000000..0eac5c48d --- /dev/null +++ b/examples/ts-react-chat/src/routes/issue-176-tool-result.tsx @@ -0,0 +1,292 @@ +import { useMemo, useState } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' +import { clientTools } from '@tanstack/ai-client' +import { modelMessagesToUIMessages, type ModelMessage } from '@tanstack/ai' +import { recommendGuitarToolDef } from '@/lib/guitar-tools' + +const modelMessages: Array = [ + { + role: 'assistant', + content: 'Let me check the weather.', + toolCalls: [ + { + id: 'issue-176-tool-call', + type: 'function', + function: { + name: 'getWeather', + arguments: '{"city":"NYC"}', + }, + }, + ], + }, + { + role: 'tool', + content: '{"temp":72,"condition":"sunny"}', + toolCallId: 'issue-176-tool-call', + }, +] + +function Issue176ToolResultRepro() { + const [prompt, setPrompt] = useState( + 'I want an acoustic guitar recommendation. Use the required tools.', + ) + const initialMessages = useMemo( + () => modelMessagesToUIMessages(modelMessages), + [], + ) + const liveTools = useMemo( + () => + clientTools( + recommendGuitarToolDef.client(({ id }) => ({ + id: Number(id), + })), + ), + [], + ) + + const { messages: fixtureMessages } = useChat({ + id: 'issue-176-tool-result-repro', + connection: fetchServerSentEvents('/api/tanchat'), + initialMessages, + }) + const { + messages: liveMessages, + sendMessage, + isLoading, + error, + } = useChat({ + id: 'issue-176-live-tool-result-repro', + connection: fetchServerSentEvents('/api/tanchat'), + tools: liveTools, + body: { + provider: 'openai', + model: 'gpt-4o', + }, + }) + + const toolCall = fixtureMessages + .flatMap((message) => message.parts) + .find( + (part) => part.type === 'tool-call' && part.id === 'issue-176-tool-call', + ) + const toolResult = fixtureMessages + .flatMap((message) => message.parts) + .find( + (part) => + part.type === 'tool-result' && + part.toolCallId === 'issue-176-tool-call', + ) + const isFixed = + toolCall?.type === 'tool-call' && + toolCall.state === 'complete' && + toolCall.output !== undefined + const liveServerToolCall = liveMessages + .flatMap((message) => message.parts) + .find((part) => part.type === 'tool-call' && part.name === 'getGuitars') + const liveServerToolResult = liveMessages + .flatMap((message) => message.parts) + .find( + (part) => + part.type === 'tool-result' && + liveServerToolCall?.type === 'tool-call' && + part.toolCallId === liveServerToolCall.id, + ) + const isLiveFixed = + liveServerToolCall?.type === 'tool-call' && + liveServerToolCall.state === 'complete' && + liveServerToolCall.output !== undefined + + return ( +
+
+
+

+ Issue #176 manual repro +

+

+ Server tool result hydration +

+

+ This page initializes a chat from model-message history containing + an assistant server tool call followed by a matching tool result. + The original tool-call part should be complete and include output. +

+
+ +
+
+

Live LLM repro

+
+
+
+