@@ -327,6 +327,31 @@ type FallbackRunnerParams = {
327327type EmbeddedAgentParams = {
328328 lifecycleGeneration ?: string ;
329329 onExecutionStarted ?: ( info ?: { lifecycleGeneration ?: string } ) => void ;
330+ onExecutionPhase ?: ( info : {
331+ phase :
332+ | "runner_entered"
333+ | "workspace"
334+ | "runtime_plugins"
335+ | "before_agent_reply"
336+ | "model_resolution"
337+ | "auth"
338+ | "context_engine"
339+ | "attempt_dispatch"
340+ | "context_assembled"
341+ | "turn_accepted"
342+ | "process_spawned"
343+ | "tool_execution_started"
344+ | "assistant_output_started"
345+ | "model_call_started" ;
346+ provider ?: string ;
347+ model ?: string ;
348+ backend ?: string ;
349+ source ?: string ;
350+ tool ?: string ;
351+ toolCallId ?: string ;
352+ itemId ?: string ;
353+ firstModelCallStarted ?: boolean ;
354+ } ) => void ;
330355 onBlockReply ?: ( payload : { text ?: string ; mediaUrls ?: string [ ] } ) => Promise < void > | void ;
331356 onToolResult ?: ( payload : { text ?: string ; mediaUrls ?: string [ ] } ) => Promise < void > | void ;
332357 onItemEvent ?: ( payload : {
@@ -361,6 +386,7 @@ function createMockTypingSignaler(): TypingSignaler {
361386 signalTextDelta : vi . fn ( async ( ) => { } ) ,
362387 signalReasoningDelta : vi . fn ( async ( ) => { } ) ,
363388 signalToolStart : vi . fn ( async ( ) => { } ) ,
389+ signalExecutionActivity : vi . fn ( async ( ) => { } ) ,
364390 } ;
365391}
366392
@@ -515,6 +541,7 @@ function createMinimalRunAgentTurnParams(overrides?: {
515541 opts ?: GetReplyOptions ;
516542 replyOperation ?: ReplyOperation ;
517543 sessionCtx ?: TemplateContext ;
544+ typingSignals ?: TypingSignaler ;
518545} ) {
519546 return {
520547 commandBody : "fix it" ,
@@ -527,7 +554,7 @@ function createMinimalRunAgentTurnParams(overrides?: {
527554 } as unknown as TemplateContext ) ,
528555 opts : overrides ?. opts ?? ( { } satisfies GetReplyOptions ) ,
529556 replyOperation : overrides ?. replyOperation ,
530- typingSignals : createMockTypingSignaler ( ) ,
557+ typingSignals : overrides ?. typingSignals ?? createMockTypingSignaler ( ) ,
531558 blockReplyPipeline : null ,
532559 blockStreamingEnabled : false ,
533560 resolvedBlockStreamingBreak : "message_end" as const ,
@@ -1327,6 +1354,73 @@ describe("runAgentTurnWithFallback", () => {
13271354 } ) ;
13281355 } ) ;
13291356
1357+ it ( "signals typing from embedded harness execution phases before assistant text" , async ( ) => {
1358+ const typingSignals = createMockTypingSignaler ( ) ;
1359+ const onAgentRunStart = vi . fn ( ) ;
1360+ state . runEmbeddedAgentMock . mockImplementationOnce ( async ( params : EmbeddedAgentParams ) => {
1361+ params . onExecutionPhase ?.( {
1362+ phase : "model_call_started" ,
1363+ provider : "openai" ,
1364+ model : "gpt-5.4" ,
1365+ firstModelCallStarted : true ,
1366+ } ) ;
1367+ return { payloads : [ { text : "final" } ] , meta : { } } ;
1368+ } ) ;
1369+
1370+ const runAgentTurnWithFallback = await getRunAgentTurnWithFallback ( ) ;
1371+ const result = await runAgentTurnWithFallback ( {
1372+ ...createMinimalRunAgentTurnParams ( {
1373+ opts : {
1374+ onAgentRunStart,
1375+ } satisfies GetReplyOptions ,
1376+ } ) ,
1377+ typingSignals,
1378+ } ) ;
1379+
1380+ expect ( result . kind ) . toBe ( "success" ) ;
1381+ expect ( typingSignals . signalExecutionActivity ) . toHaveBeenCalledOnce ( ) ;
1382+ expect ( typingSignals . signalRunStart ) . not . toHaveBeenCalled ( ) ;
1383+ expect ( onAgentRunStart ) . toHaveBeenCalledOnce ( ) ;
1384+ } ) ;
1385+
1386+ it ( "forwards CLI harness execution phases into typing signals" , async ( ) => {
1387+ state . isCliProviderMock . mockReturnValue ( true ) ;
1388+ state . runWithModelFallbackMock . mockImplementationOnce ( async ( params : FallbackRunnerParams ) => ( {
1389+ result : await params . run ( "codex-cli" , "gpt-5.4" ) ,
1390+ provider : "codex-cli" ,
1391+ model : "gpt-5.4" ,
1392+ attempts : [ ] ,
1393+ } ) ) ;
1394+ state . runCliAgentMock . mockImplementationOnce ( async ( params : EmbeddedAgentParams ) => {
1395+ params . onExecutionPhase ?.( {
1396+ phase : "process_spawned" ,
1397+ provider : "codex-cli" ,
1398+ model : "gpt-5.4" ,
1399+ backend : "codex" ,
1400+ } ) ;
1401+ return { payloads : [ { text : "final" } ] , meta : { } } ;
1402+ } ) ;
1403+ const followupRun = createFollowupRun ( ) ;
1404+ followupRun . run . provider = "codex-cli" ;
1405+ followupRun . run . model = "gpt-5.4" ;
1406+ const typingSignals = createMockTypingSignaler ( ) ;
1407+
1408+ const runAgentTurnWithFallback = await getRunAgentTurnWithFallback ( ) ;
1409+ const result = await runAgentTurnWithFallback (
1410+ createMinimalRunAgentTurnParams ( {
1411+ followupRun,
1412+ typingSignals,
1413+ } ) ,
1414+ ) ;
1415+
1416+ expect ( result . kind ) . toBe ( "success" ) ;
1417+ expect ( typingSignals . signalExecutionActivity ) . toHaveBeenCalledOnce ( ) ;
1418+ expectMockCallArgFields ( state . runCliAgentMock , 0 , "CLI run params" , {
1419+ provider : "codex-cli" ,
1420+ model : "gpt-5.4" ,
1421+ } ) ;
1422+ } ) ;
1423+
13301424 it ( "registers run ownership before asynchronous image preflight" , async ( ) => {
13311425 const agentEvents = await import ( "../../infra/agent-events.js" ) ;
13321426 const registerAgentRunContext = vi . mocked ( agentEvents . registerAgentRunContext ) ;
0 commit comments