@@ -4,6 +4,7 @@ import { addLanguageModelUsage, createNullLanguageModelUsage } from 'ai/internal
44import {
55 createAssistantMessage ,
66 createUserMessage ,
7+ getRetryableMessage ,
78 withMessageStatus ,
89 type Conversation ,
910 type ConversationMessage ,
@@ -19,7 +20,7 @@ import { resolveContextAttachments } from './context';
1920
2021type ActiveRequest = {
2122 controller : AbortController ;
22- turnId : string ;
23+ messageId : string ;
2324} ;
2425
2526export class ConversationRunner {
@@ -31,15 +32,16 @@ export class ConversationRunner {
3132 private readonly publishState : ( ) => Promise < void > ,
3233 ) { }
3334
34- async send ( input : { text : string } | { action : ChatQuickAction } ) : Promise < void > {
35+ async send ( input : { text : string } | { action : ChatQuickAction } | { retryMessageId : string } ) : Promise < void > {
3536 if ( ( 'text' in input && ! input . text . trim ( ) ) || this . preparation ) return ;
3637
3738 const [ promptsConfig , config ] = await Promise . all ( [
3839 this . stores . promptsConfig . get ( ) ,
3940 this . stores . modelConfigs . getSelected ( ) ,
4041 ] ) ;
4142 const prompts = resolvePrompts ( promptsConfig ) ;
42- const text = 'text' in input ? input . text : prompts . quickActions [ input . action ] ;
43+ const text = 'text' in input ? input . text : 'action' in input ? prompts . quickActions [ input . action ] : '' ;
44+ const retry = 'retryMessageId' in input ;
4345
4446 if ( ! config ) {
4547 await this . stores . runtime . setIn ( 'chat.notice' , {
@@ -50,36 +52,38 @@ export class ConversationRunner {
5052 }
5153 const runtime = await this . stores . runtime . get ( ) ;
5254 if ( this . preparation ) return ;
55+ const previous = runtime . chat . conversation ;
56+ if ( retry && ( ! previous || getRetryableMessage ( previous . messages ) ?. id !== input . retryMessageId ) ) return ;
57+ const conversationId = previous ?. id ?? globalThis . crypto . randomUUID ( ) ;
58+ if ( this . requests . has ( conversationId ) ) return ;
5359 const descriptors = ( runtime . chat . pendingAttachments ?? [ ] ) . map ( ( descriptor ) => ( { ...descriptor } ) ) ;
5460 const preparation = Symbol ( 'conversation-preparation' ) ;
5561 this . preparation = preparation ;
5662 await this . stores . runtime . setIn ( 'chat' , { ...runtime . chat , preparing : true , notice : undefined } ) ;
5763
58- let attachments ;
64+ let userMessage : ConversationMessage ;
5965 try {
6066 await this . publishState ( ) ;
61- attachments = await resolveContextAttachments ( descriptors ) ;
67+ if ( retry ) {
68+ userMessage = previous ! . messages . at ( - 2 ) ! ;
69+ } else {
70+ const attachments = await resolveContextAttachments ( descriptors ) ;
71+ const recentFiles = runtime . chat . includeRecentFiles === false ? [ ] : runtime . chat . recentFiles ;
72+ userMessage = createUserMessage ( globalThis . crypto . randomUUID ( ) , text , attachments , recentFiles ) ;
73+ }
6274 } catch ( error ) {
6375 const detail = error instanceof Error ? error . message : 'Unknown error.' ;
6476 await this . finishPreparation ( preparation , `Unable to read the selected context attachment. ${ detail } ` ) ;
6577 return ;
6678 }
6779 if ( this . preparation !== preparation ) return ;
6880
69- const previous = runtime . chat . conversation ;
7081 const startedAt = Date . now ( ) ;
71- const conversationId = previous ?. id ?? globalThis . crypto . randomUUID ( ) ;
72- if ( this . requests . has ( conversationId ) ) {
73- await this . finishPreparation ( preparation ) ;
74- return ;
75- }
76-
77- const turnId = globalThis . crypto . randomUUID ( ) ;
78- const recentFiles = runtime . chat . includeRecentFiles === false ? [ ] : runtime . chat . recentFiles ;
79- const userMessage = createUserMessage ( turnId , text , attachments , recentFiles ) ;
82+ const turnId = userMessage . metadata . turnId ;
83+ const history = retry ? previous ! . messages . slice ( 0 , - 2 ) : ( previous ?. messages ?? [ ] ) ;
8084 let providerMessages ;
8185 try {
82- providerMessages = await buildModelMessages ( previous ?. messages ?? [ ] , [ userMessage ] ) ;
86+ providerMessages = await buildModelMessages ( history , [ userMessage ] ) ;
8387 } catch ( error ) {
8488 const detail = error instanceof Error ? error . message : 'Unknown error.' ;
8589 await this . finishPreparation ( preparation , `Unable to prepare the conversation. ${ detail } ` ) ;
@@ -91,7 +95,7 @@ export class ConversationRunner {
9195 const conversation : Conversation = previous
9296 ? {
9397 ...previous ,
94- messages : [ ...previous . messages , userMessage , assistantMessage ] ,
98+ messages : [ ...history , userMessage , assistantMessage ] ,
9599 updatedAt : startedAt ,
96100 }
97101 : {
@@ -116,15 +120,15 @@ export class ConversationRunner {
116120 return ;
117121 }
118122 if ( this . preparation !== preparation ) {
119- await this . finishTurn ( conversationId , turnId , 'aborted' ) ;
123+ await this . setMessageStatus ( conversationId , assistantMessage . id , 'aborted' ) ;
120124 return ;
121125 }
122126
123127 this . preparation = undefined ;
124128 const abortController = new AbortController ( ) ;
125129 const request : ActiveRequest = {
126130 controller : abortController ,
127- turnId ,
131+ messageId : assistantMessage . id ,
128132 } ;
129133 this . requests . set ( conversationId , request ) ;
130134 let mcp : Awaited < ReturnType < typeof connectMcpTools > > | undefined ;
@@ -134,7 +138,7 @@ export class ConversationRunner {
134138 await this . stores . runtime . setIn ( 'chat' , {
135139 ...currentRuntime . chat ,
136140 conversation,
137- pendingAttachments : [ ] ,
141+ pendingAttachments : retry ? currentRuntime . chat . pendingAttachments : [ ] ,
138142 preparing : false ,
139143 } ) ;
140144 await this . stores . conversations . select ( conversationId ) ;
@@ -154,16 +158,16 @@ export class ConversationRunner {
154158 if ( ! abortController . signal . aborted && this . requests . get ( conversationId ) === request ) {
155159 const usage = await agentStream . usage ;
156160 if ( ! abortController . signal . aborted && this . requests . get ( conversationId ) === request ) {
157- await this . completeTurn ( conversationId , turnId , usage ) ;
161+ await this . setMessageStatus ( conversationId , request . messageId , 'completed' , undefined , usage ) ;
158162 }
159163 }
160164 } catch ( error ) {
161165 if ( this . requests . get ( conversationId ) !== request ) return ;
162166 if ( abortController . signal . aborted ) {
163- await this . finishTurn ( conversationId , turnId , 'aborted' ) ;
167+ await this . setMessageStatus ( conversationId , request . messageId , 'aborted' ) ;
164168 } else {
165169 const message = error instanceof Error ? error . message : 'Unknown error.' ;
166- await this . finishTurn ( conversationId , turnId , 'failed' , message ) ;
170+ await this . setMessageStatus ( conversationId , request . messageId , 'failed' , message ) ;
167171 }
168172 } finally {
169173 if ( this . requests . get ( conversationId ) === request ) this . requests . delete ( conversationId ) ;
@@ -185,7 +189,7 @@ export class ConversationRunner {
185189 if ( ! request ) return ;
186190 this . requests . delete ( conversationId ) ;
187191 request . controller . abort ( ) ;
188- await this . finishTurn ( conversationId , request . turnId , 'aborted' ) ;
192+ await this . setMessageStatus ( conversationId , request . messageId , 'aborted' ) ;
189193 }
190194
191195 isRequestActive ( conversationId : string | undefined ) : boolean {
@@ -218,32 +222,17 @@ export class ConversationRunner {
218222 await this . publishState ( ) ;
219223 }
220224
221- private async completeTurn ( conversationId : string , turnId : string , usage : LanguageModelUsage ) : Promise < void > {
222- await this . setTurnStatus ( conversationId , turnId , 'completed' , undefined , usage ) ;
223- }
224-
225- private async finishTurn (
226- conversationId : string ,
227- turnId : string ,
228- status : 'aborted' | 'failed' ,
229- error ?: string ,
230- ) : Promise < void > {
231- await this . setTurnStatus ( conversationId , turnId , status , error ) ;
232- }
233-
234- private async setTurnStatus (
225+ private async setMessageStatus (
235226 conversationId : string ,
236- turnId : string ,
227+ messageId : string ,
237228 status : 'completed' | 'aborted' | 'failed' ,
238229 error ?: string ,
239230 usage ?: LanguageModelUsage ,
240231 ) : Promise < void > {
241232 const conversation = await this . stores . conversations . get ( conversationId ) ;
242- if ( ! conversation ) return ;
233+ if ( ! conversation ?. messages . some ( ( message ) => message . id === messageId ) ) return ;
243234 const messages = conversation . messages . map ( ( message ) =>
244- message . role === 'assistant' && message . metadata . turnId === turnId
245- ? withMessageStatus ( message , status , error )
246- : message ,
235+ message . id === messageId ? withMessageStatus ( message , status , error ) : message ,
247236 ) ;
248237 const updatedAt = Date . now ( ) ;
249238 const nextUsage = usage === undefined ? undefined : addUsage ( conversation . usage , usage ) ;
0 commit comments