@@ -250,15 +250,20 @@ export namespace SessionPrompt {
250250 let msgs = await MessageV2 . filterCompacted ( MessageV2 . stream ( sessionID ) )
251251
252252 let lastUser : MessageV2 . User | undefined
253- let lastAssistant : MessageV2 . Assistant | undefined
253+ let lastAssistant : MessageV2 . WithParts | undefined
254254 let lastFinished : MessageV2 . Assistant | undefined
255255 let tasks : ( MessageV2 . CompactionPart | MessageV2 . SubtaskPart ) [ ] = [ ]
256256 for ( let i = msgs . length - 1 ; i >= 0 ; i -- ) {
257257 const msg = msgs [ i ]
258- if ( ! lastUser && msg . info . role === "user" ) lastUser = msg . info as MessageV2 . User
259- if ( ! lastAssistant && msg . info . role === "assistant" ) lastAssistant = msg . info as MessageV2 . Assistant
260- if ( ! lastFinished && msg . info . role === "assistant" && msg . info . finish )
258+ if ( ! lastUser && msg . info . role === "user" ) {
259+ lastUser = msg . info as MessageV2 . User
260+ }
261+ if ( ! lastAssistant && msg . info . role === "assistant" ) {
262+ lastAssistant = msg
263+ }
264+ if ( ! lastFinished && msg . info . role === "assistant" && msg . info . finish ) {
261265 lastFinished = msg . info as MessageV2 . Assistant
266+ }
262267 if ( lastUser && lastFinished ) break
263268 const task = msg . parts . filter ( ( part ) => part . type === "compaction" || part . type === "subtask" )
264269 if ( task && ! lastFinished ) {
@@ -267,13 +272,38 @@ export namespace SessionPrompt {
267272 }
268273
269274 if ( ! lastUser ) throw new Error ( "No user message found in stream. This should never happen." )
270- if (
271- lastAssistant ?. finish &&
272- ! [ "tool-calls" , "unknown" ] . includes ( lastAssistant . finish ) &&
273- lastUser . id < lastAssistant . id
274- ) {
275- log . info ( "exiting loop" , { sessionID } )
276- break
275+
276+ // Exit Loop When:
277+ // • Text + Reasoning → Complete response ready
278+ // • Text Only → Normal response ready
279+ //
280+ // Continue Loop When:
281+ // • Reasoning Only → Wait for text content
282+ // • Any tool calls pending → Tools must execute first
283+ // • Text + Tools → Tools executing needed
284+ // • Invalid/error state → Unexpected condition
285+
286+ const hasReasoningBlocks = lastAssistant ?. parts . some ( ( part ) => part . type === "reasoning" )
287+ const hasTextContent = lastAssistant ?. parts . some ( ( part ) => part . type === "text" )
288+ const hasOnlyReasoning = hasReasoningBlocks && ! hasTextContent
289+
290+ // Check if there are pending or running tools for the last assistant message
291+ const hasPendingTools = lastAssistant
292+ ? await hasRunningOrPendingTools ( ( lastAssistant . info as MessageV2 . Assistant ) . id )
293+ : false
294+
295+ if ( lastAssistant ?. info . role === "assistant" && lastAssistant . info . finish ) {
296+ const hasToolCalls = lastAssistant . info . finish === "tool-calls"
297+ const hasTextAndToolCalls = hasTextContent && hasToolCalls
298+ const hasValidFinish = ! [ "tool-calls" , "unknown" ] . includes ( lastAssistant . info . finish )
299+ const hasValidIds = ( lastUser as MessageV2 . User ) . id < ( lastAssistant . info as MessageV2 . Assistant ) . id
300+ const shouldExit =
301+ hasValidFinish && hasValidIds && ! hasOnlyReasoning && ! hasTextAndToolCalls && ! hasPendingTools
302+
303+ if ( shouldExit ) {
304+ log . info ( "exiting loop" , { sessionID } )
305+ break
306+ }
277307 }
278308
279309 step ++
@@ -360,7 +390,7 @@ export namespace SessionPrompt {
360390 } ,
361391 } ,
362392 )
363- . catch ( ( ) => { } )
393+ . catch ( ( ) => { } )
364394 assistantMessage . finish = "tool-calls"
365395 assistantMessage . time . completed = Date . now ( )
366396 await Session . updateMessage ( assistantMessage )
@@ -539,10 +569,10 @@ export namespace SessionPrompt {
539569 headers : {
540570 ...( model . providerID . startsWith ( "opencode" )
541571 ? {
542- "x-opencode-project" : Instance . project . id ,
543- "x-opencode-session" : sessionID ,
544- "x-opencode-request" : lastUser . id ,
545- }
572+ "x-opencode-project" : Instance . project . id ,
573+ "x-opencode-session" : sessionID ,
574+ "x-opencode-request" : lastUser . id ,
575+ }
546576 : undefined ) ,
547577 ...model . headers ,
548578 } ,
@@ -914,7 +944,7 @@ export namespace SessionPrompt {
914944 agent : input . agent ! ,
915945 messageID : info . id ,
916946 extra : { bypassCwdCheck : true , model } ,
917- metadata : async ( ) => { } ,
947+ metadata : async ( ) => { } ,
918948 } )
919949 pieces . push ( {
920950 id : Identifier . ascending ( "part" ) ,
@@ -974,7 +1004,7 @@ export namespace SessionPrompt {
9741004 agent : input . agent ! ,
9751005 messageID : info . id ,
9761006 extra : { bypassCwdCheck : true } ,
977- metadata : async ( ) => { } ,
1007+ metadata : async ( ) => { } ,
9781008 } ) ,
9791009 )
9801010 return [
@@ -1377,14 +1407,14 @@ export namespace SessionPrompt {
13771407 const parts =
13781408 ( agent . mode === "subagent" && command . subtask !== false ) || command . subtask === true
13791409 ? [
1380- {
1381- type : "subtask" as const ,
1382- agent : agent . name ,
1383- description : command . description ?? "" ,
1384- // TODO: how can we make task tool accept a more complex input?
1385- prompt : await resolvePromptParts ( template ) . then ( ( x ) => x . find ( ( y ) => y . type === "text" ) ?. text ?? "" ) ,
1386- } ,
1387- ]
1410+ {
1411+ type : "subtask" as const ,
1412+ agent : agent . name ,
1413+ description : command . description ?? "" ,
1414+ // TODO: how can we make task tool accept a more complex input?
1415+ prompt : await resolvePromptParts ( template ) . then ( ( x ) => x . find ( ( y ) => y . type === "text" ) ?. text ?? "" ) ,
1416+ } ,
1417+ ]
13881418 : await resolvePromptParts ( template )
13891419
13901420 const result = ( await prompt ( {
@@ -1405,6 +1435,18 @@ export namespace SessionPrompt {
14051435 return result
14061436 }
14071437
1438+ async function hasRunningOrPendingTools ( messageID : string ) : Promise < boolean > {
1439+ try {
1440+ const parts = await MessageV2 . parts ( messageID )
1441+ return parts . some (
1442+ ( part ) => part . type === "tool" && ( part . state . status === "running" || part . state . status === "pending" ) ,
1443+ )
1444+ } catch {
1445+ // If we can't read the parts, assume no pending tools
1446+ return false
1447+ }
1448+ }
1449+
14081450 async function ensureTitle ( input : {
14091451 session : Session . Info
14101452 message : MessageV2 . WithParts
0 commit comments