@@ -560,6 +560,106 @@ test("runAssistantWithTools calls onBeforeNextTurn only for toolUse turns with t
560560 ) ;
561561} ) ;
562562
563+ test ( "runAssistantWithTools announces execution start before invoking the tool executor" , async ( ) => {
564+ const askToolCall = createToolCall ( "call-ask-order" , "AskUserQuestion" , {
565+ questions : [
566+ {
567+ id : "choice" ,
568+ prompt : "Choose one" ,
569+ options : [ { label : "First" } , { label : "Second" } ] ,
570+ } ,
571+ ] ,
572+ } ) ;
573+ const askTool = {
574+ name : "AskUserQuestion" ,
575+ description : "Ask the user" ,
576+ parameters : { type : "object" , properties : { } } ,
577+ } ;
578+ const sequence = [ ] ;
579+ resetFakeStreams ( createToolUseAssistant ( askToolCall ) , createTextAssistant ( "done" ) ) ;
580+ const { params } = createBaseParams ( {
581+ context : {
582+ systemPrompt : "Base system prompt" ,
583+ messages : [ { role : "user" , content : "Start" , timestamp : 1 } ] ,
584+ tools : [ askTool ] ,
585+ } ,
586+ tools : [ askTool ] ,
587+ onToolCall ( ) {
588+ sequence . push ( "tool_call" ) ;
589+ } ,
590+ onToolExecutionStart ( ) {
591+ sequence . push ( "execution_start" ) ;
592+ } ,
593+ async executeToolCall ( toolCall ) {
594+ sequence . push ( "execute" ) ;
595+ return createToolResult ( toolCall ) ;
596+ } ,
597+ } ) ;
598+
599+ await runAssistantWithTools ( params ) ;
600+
601+ const executionStartIndex = sequence . indexOf ( "execution_start" ) ;
602+ const executeIndex = sequence . indexOf ( "execute" ) ;
603+ assert . ok ( sequence . includes ( "tool_call" ) ) ;
604+ assert . ok ( executionStartIndex >= 0 ) ;
605+ assert . ok ( executeIndex > executionStartIndex ) ;
606+ } ) ;
607+
608+ test ( "AskUserQuestion is pending before the next user-event task after execution start" , async ( ) => {
609+ const askTools = loader . loadModule ( "src/lib/tools/askUserQuestionTools.ts" ) ;
610+ const bundle = askTools . createAskUserQuestionTools ( {
611+ conversationId : "conversation-runner" ,
612+ timeoutMs : 200 ,
613+ } ) ;
614+ const askTool = bundle . tools . find ( ( tool ) => tool . name === "AskUserQuestion" ) ;
615+ assert . ok ( askTool ) ;
616+ const askToolCall = createToolCall ( "call-ask-next-task" , "AskUserQuestion" , {
617+ questions : [
618+ {
619+ id : "choice" ,
620+ prompt : "Choose one" ,
621+ options : [ { label : "First" , recommended : true } , { label : "Second" } ] ,
622+ } ,
623+ ] ,
624+ } ) ;
625+ resetFakeStreams ( createToolUseAssistant ( askToolCall ) , createTextAssistant ( "done" ) ) ;
626+
627+ let resolveAnswerAttempt ;
628+ const answerAttempt = new Promise ( ( resolve ) => {
629+ resolveAnswerAttempt = resolve ;
630+ } ) ;
631+ const { params } = createBaseParams ( {
632+ context : {
633+ systemPrompt : "Base system prompt" ,
634+ messages : [ { role : "user" , content : "Start" , timestamp : 1 } ] ,
635+ tools : [ askTool ] ,
636+ } ,
637+ tools : [ askTool ] ,
638+ executeToolCall : bundle . executeToolCall ,
639+ onToolExecutionStart ( ) {
640+ // DOM clicks and Gateway deliveries cannot run inside this synchronous callback;
641+ // the earliest real user event is the next task, after the runner has entered
642+ // executeToolCall and synchronously populated pendingByToolCallId.
643+ setImmediate ( ( ) => {
644+ resolveAnswerAttempt (
645+ askTools . answerAskUserQuestion ( "call-ask-next-task" , [
646+ { questionId : "choice" , selectedLabel : "Second" } ,
647+ ] ) ,
648+ ) ;
649+ } ) ;
650+ } ,
651+ } ) ;
652+
653+ const result = await runAssistantWithTools ( params ) ;
654+ assert . deepEqual ( await answerAttempt , { ok : true } ) ;
655+ const toolResult = result . emittedMessages . find (
656+ ( message ) => message . role === "toolResult" && message . toolCallId === askToolCall . id ,
657+ ) ;
658+ assert . ok ( toolResult ) ;
659+ assert . equal ( toolResult . details . answers [ 0 ] . selectedLabel , "Second" ) ;
660+ assert . equal ( "timedOut" in toolResult . details , false ) ;
661+ } ) ;
662+
563663// Mocked turn tests (agent-turn-cancelled-history.test.mjs) replay this payload
564664// shape by hand; the assertions here are what keep those replicas honest.
565665test ( "runAssistantWithTools reports 1-based monotonic rounds and paired tool results to onBeforeNextTurn" , async ( ) => {
0 commit comments