* Stream a response from the LLM. * Returns true if the stream completed successfully, false on abort or error.
()
| 847 | * Returns true if the stream completed successfully, false on abort or error. |
| 848 | */ |
| 849 | private async streamResponse(): Promise<boolean> { |
| 850 | // Guard against concurrent streams - if already loading, skip |
| 851 | if (this.isLoading) { |
| 852 | return false |
| 853 | } |
| 854 | |
| 855 | // Track generation so a superseded stream's cleanup doesn't clobber the new one |
| 856 | const generation = ++this.streamGeneration |
| 857 | const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` |
| 858 | this.currentRunId = runId |
| 859 | |
| 860 | this.setIsLoading(true) |
| 861 | this.setStatus('submitted') |
| 862 | this.setError(undefined) |
| 863 | this.errorReportedGeneration = null |
| 864 | this.abortController = new AbortController() |
| 865 | // Capture the signal immediately so that a concurrent stop() or |
| 866 | // sendMessage() that reassigns this.abortController cannot cause |
| 867 | // connect() to receive a stale or null signal. |
| 868 | const signal = this.abortController.signal |
| 869 | // Reset pending tool executions for the new stream |
| 870 | this.pendingToolExecutions.clear() |
| 871 | let streamCompletedSuccessfully = false |
| 872 | let activeDevtoolsRunId: string | null = null |
| 873 | let runTerminalEventEmitted = false |
| 874 | |
| 875 | try { |
| 876 | // Get UIMessages with parts (preserves approval state and client tool results) |
| 877 | const messages = this.processor.getMessages() |
| 878 | const clientTools = new Map(this.clientToolsRef.current) |
| 879 | const runtimeContext = this.context |
| 880 | |
| 881 | // Call onResponse callback |
| 882 | await this.callbacksRef.current.onResponse() |
| 883 | |
| 884 | // If the stream was cancelled during the onResponse await (e.g. stop() |
| 885 | // from a callback or unmount, or reload() superseding this stream), |
| 886 | // bail out before allocating waitForProcessing() — otherwise the |
| 887 | // resolveProcessing() that ran during cancellation is a no-op and the |
| 888 | // await processingComplete below would deadlock. |
| 889 | if (signal.aborted) { |
| 890 | return false |
| 891 | } |
| 892 | |
| 893 | // Merge sources for the wire `forwardedProps` field, in priority |
| 894 | // order (later spreads win): |
| 895 | // 1. Legacy `body` option (deprecated). |
| 896 | // 2. Canonical `forwardedProps` option (wins over `body`). |
| 897 | // 3. Per-message `body` arg passed to `sendMessage` (highest). |
| 898 | // The AG-UI standard `threadId` is sent at the wire's top level for |
| 899 | // run/conversation correlation, so we no longer auto-emit a separate |
| 900 | // `conversationId` here — `chat({ threadId })` server-side covers the |
| 901 | // same role for devtools/observability. |
| 902 | const mergedBody = { |
| 903 | ...this.bodyOption, |
| 904 | ...this.forwardedPropsOption, |
| 905 | ...this.pendingMessageBody, |
| 906 | } |
no test coverage detected