@@ -12,13 +12,21 @@ import type { McpContext } from "../context.js";
1212
1313// ─── In-memory chat sessions ──────────────────────────────────────
1414
15+ type ChatMessage = {
16+ id : string ;
17+ role : string ;
18+ parts : Array < { type : string ; [ key : string ] : unknown } > ;
19+ } ;
20+
1521type ChatSession = {
1622 runId : string ;
1723 chatId : string ;
1824 agentId : string ;
1925 lastEventId ?: string ;
2026 apiClient : ApiClient ;
2127 clientData ?: Record < string , unknown > ;
28+ /** Accumulated conversation messages for continuation payloads. */
29+ messages : ChatMessage [ ] ;
2230} ;
2331
2432const activeSessions = new Map < string , ChatSession > ( ) ;
@@ -108,6 +116,7 @@ export const startAgentChatTool = {
108116 agentId : input . agentId ,
109117 apiClient,
110118 clientData : input . clientData ,
119+ messages : [ ] ,
111120 } ) ;
112121
113122 return {
@@ -134,6 +143,7 @@ export const startAgentChatTool = {
134143 agentId : input . agentId ,
135144 apiClient,
136145 clientData : input . clientData ,
146+ messages : [ ] ,
137147 } ) ;
138148
139149 return {
@@ -176,10 +186,15 @@ export const sendAgentMessageTool = {
176186 }
177187
178188 const msgId = `msg-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
189+ const userMessage : ChatMessage = {
190+ id : msgId , role : "user" , parts : [ { type : "text" , text : input . message } ] ,
191+ } ;
192+
193+ // Track the outgoing user message
194+ session . messages . push ( userMessage ) ;
195+
179196 const messagePayload = {
180- messages : [
181- { id : msgId , role : "user" , parts : [ { type : "text" , text : input . message } ] } ,
182- ] ,
197+ messages : [ userMessage ] ,
183198 chatId : session . chatId ,
184199 trigger : "submit-message" ,
185200 metadata : session . clientData ,
@@ -194,9 +209,16 @@ export const sendAgentMessageTool = {
194209 messagePayload
195210 ) ;
196211 } catch ( sendErr : any ) {
197- // Run may have ended — trigger a new one
212+ // Run may have ended — trigger a new one with full history
198213 const result = await session . apiClient . triggerTask ( session . agentId , {
199- payload : { ...messagePayload , continuation : true , previousRunId : session . runId } ,
214+ payload : {
215+ messages : session . messages ,
216+ chatId : session . chatId ,
217+ trigger : "submit-message" ,
218+ metadata : session . clientData ,
219+ continuation : true ,
220+ previousRunId : session . runId ,
221+ } ,
200222 options : {
201223 payloadType : "application/json" ,
202224 tags : [ `chat:${ session . chatId } ` ] ,
@@ -218,17 +240,16 @@ export const sendAgentMessageTool = {
218240 }
219241
220242 // Subscribe to the response stream and collect the full text
221- const { text, toolCalls } = await collectAgentResponse ( session ) ;
243+ const { text, toolCalls, assistantMessage } = await collectAgentResponse ( session ) ;
222244
223- const contents = [ text ] ;
245+ // Track the assistant response for continuation payloads
246+ session . messages . push ( assistantMessage ) ;
224247
225- if ( toolCalls . length > 0 ) {
226- contents . push ( "" ) ;
227- contents . push ( `Tools used: ${ toolCalls . join ( ", " ) } ` ) ;
228- }
248+ const formatted = formatAssistantParts ( assistantMessage . parts ) ;
249+ const footer = `\n\n---\nRun: ${ session . runId } ` ;
229250
230251 return {
231- content : [ { type : "text" , text : contents . join ( "\n" ) } ] ,
252+ content : [ { type : "text" , text : formatted + footer } ] ,
232253 } ;
233254 } ) ,
234255} ;
@@ -287,7 +308,7 @@ export const closeAgentChatTool = {
287308
288309async function collectAgentResponse (
289310 session : ChatSession
290- ) : Promise < { text : string ; toolCalls : string [ ] } > {
311+ ) : Promise < { text : string ; toolCalls : string [ ] ; assistantMessage : ChatMessage } > {
291312 const baseURL = session . apiClient . baseUrl ;
292313 const streamUrl = `${ baseURL } /realtime/v1/streams/${ session . runId } /${ CHAT_STREAM_KEY } ` ;
293314
@@ -299,15 +320,14 @@ async function collectAgentResponse(
299320 lastEventId : session . lastEventId ,
300321 } ) ;
301322
302- try {
303- sseStream = await subscription . subscribe ( ) ;
304- } catch ( err : any ) {
305- throw err ;
306- }
323+ const sseStream = await subscription . subscribe ( ) ;
307324 const reader = sseStream . getReader ( ) ;
308325
309326 let text = "" ;
310327 const toolCalls : string [ ] = [ ] ;
328+ const parts : Array < { type : string ; [ key : string ] : unknown } > = [ ] ;
329+ // Track current text part to accumulate deltas
330+ let currentTextId : string | undefined ;
311331
312332 try {
313333 while ( true ) {
@@ -323,25 +343,117 @@ async function collectAgentResponse(
323343 if ( value . chunk != null && typeof value . chunk === "object" ) {
324344 const chunk = value . chunk as Record < string , unknown > ;
325345
326- if ( chunk . type === "__trigger_turn_complete " ) {
346+ if ( chunk . type === "trigger:turn-complete " ) {
327347 break ;
328348 }
329349
350+ if ( chunk . type === "trigger:upgrade-required" ) {
351+ // Agent requested upgrade — trigger continuation with full history
352+ const previousRunId = session . runId ;
353+ const result = await session . apiClient . triggerTask ( session . agentId , {
354+ payload : {
355+ messages : session . messages ,
356+ chatId : session . chatId ,
357+ trigger : "submit-message" ,
358+ metadata : session . clientData ,
359+ continuation : true ,
360+ previousRunId,
361+ } ,
362+ options : {
363+ payloadType : "application/json" ,
364+ tags : [ `chat:${ session . chatId } ` ] ,
365+ } ,
366+ } ) ;
367+ session . runId = result . id ;
368+ session . lastEventId = undefined ;
369+ reader . releaseLock ( ) ;
370+ // Recurse — subscribe to the new run's stream
371+ return collectAgentResponse ( session ) ;
372+ }
373+
330374 if ( chunk . type === "text-delta" && typeof chunk . delta === "string" ) {
331375 text += chunk . delta ;
376+ // Accumulate into a text part
377+ const textId = ( chunk . id as string ) ?? "text" ;
378+ if ( currentTextId !== textId ) {
379+ currentTextId = textId ;
380+ parts . push ( { type : "text" , text : chunk . delta } ) ;
381+ } else {
382+ const last = parts [ parts . length - 1 ] ;
383+ if ( last && last . type === "text" ) {
384+ last . text = ( last . text as string ) + chunk . delta ;
385+ }
386+ }
332387 }
333388
334- if (
335- chunk . type === "tool-input-available" &&
336- typeof chunk . toolName === "string"
337- ) {
389+ if ( chunk . type === "tool-input-available" && typeof chunk . toolName === "string" ) {
338390 toolCalls . push ( chunk . toolName ) ;
391+ parts . push ( {
392+ type : `tool-${ chunk . toolName } ` ,
393+ toolCallId : chunk . toolCallId as string ,
394+ toolName : chunk . toolName ,
395+ state : "input-available" ,
396+ input : chunk . input ,
397+ } ) ;
398+ }
399+
400+ if ( chunk . type === "tool-output-available" && typeof chunk . toolCallId === "string" ) {
401+ // Update existing tool part with output
402+ const toolPart = parts . find (
403+ ( p ) => p . toolCallId === chunk . toolCallId
404+ ) ;
405+ if ( toolPart ) {
406+ toolPart . state = "output-available" ;
407+ toolPart . output = chunk . output ;
408+ }
339409 }
340410 }
341411 }
342412 } finally {
343413 reader . releaseLock ( ) ;
344414 }
345415
346- return { text, toolCalls } ;
416+ const assistantMessage : ChatMessage = {
417+ id : `msg-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
418+ role : "assistant" ,
419+ parts : parts . length > 0 ? parts : [ { type : "text" , text } ] ,
420+ } ;
421+
422+ return { text, toolCalls, assistantMessage } ;
423+ }
424+
425+ // ─── Response formatter ──────────────────────────────────────────
426+
427+ function formatAssistantParts (
428+ parts : Array < { type : string ; [ key : string ] : unknown } >
429+ ) : string {
430+ const sections : string [ ] = [ ] ;
431+
432+ for ( const part of parts ) {
433+ if ( part . type === "text" && typeof part . text === "string" && part . text ) {
434+ sections . push ( part . text ) ;
435+ } else if ( part . type . startsWith ( "tool-" ) && part . toolName ) {
436+ const name = part . toolName as string ;
437+ const input = part . input ;
438+ const output = part . output ;
439+
440+ let toolSection = `[Tool: ${ name } ]` ;
441+ if ( input != null ) {
442+ toolSection += `\nInput: ${ compactJson ( input ) } ` ;
443+ }
444+ if ( output != null ) {
445+ toolSection += `\nOutput: ${ compactJson ( output ) } ` ;
446+ }
447+ sections . push ( toolSection ) ;
448+ }
449+ }
450+
451+ return sections . join ( "\n\n" ) ;
452+ }
453+
454+ function compactJson ( value : unknown ) : string {
455+ const str = JSON . stringify ( value ) ;
456+ // Keep short values inline, truncate long ones
457+ if ( str . length <= 200 ) return str ;
458+ return str . slice ( 0 , 200 ) + "…" ;
347459}
0 commit comments