@@ -29,13 +29,25 @@ export type RunEvent =
2929 | { type : 'done' ; report : RunReport }
3030 | { type : 'error' ; message : string } ;
3131
32+ // Pacing makes phase events visible to the human watching the screen.
33+ // Real LLM calls already take 2–10 s each; this is the floor so the stub
34+ // path also has breathing room. Disabled when a real provider is in use,
35+ // since model latency is the natural pacer.
36+ const PACE_PHASE_MS = Number ( process . env . SENTINEL_PACE_PHASE_MS || 600 ) ;
37+ const PACE_TURN_MS = Number ( process . env . SENTINEL_PACE_TURN_MS || 350 ) ;
38+ const sleep = ( ms : number ) => new Promise < void > ( r => setTimeout ( r , ms ) ) ;
39+
3240export async function * orchestrate ( scenario : Scenario ) : AsyncGenerator < RunEvent , void , void > {
3341 const runId = `r_${ nanoid ( 12 ) } ` ;
3442 const startedAt = Date . now ( ) ;
3543 const turns : AgentTurn [ ] = [ ] ;
3644 const signals = [ ...scenario . signals ] . sort ( ( a , b ) => a . ts - b . ts ) ;
45+ let stubInUse = false ;
3746
3847 const emit = < T extends RunEvent > ( e : T ) => e ;
48+ const phasePace = async ( ) => { if ( stubInUse && PACE_PHASE_MS > 0 ) await sleep ( PACE_PHASE_MS ) ; } ;
49+ const turnPace = async ( ) => { if ( stubInUse && PACE_TURN_MS > 0 ) await sleep ( PACE_TURN_MS ) ; } ;
50+ const trackProvider = ( t : AgentTurn ) => { if ( t . provider === 'stub' ) stubInUse = true ; } ;
3951
4052 try {
4153 yield emit ( { type : 'phase' , phase : 'ingest' } ) ;
@@ -45,16 +57,25 @@ export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent,
4557 yield emit ( { type : 'memory' , episodeIds : recalled . map ( r => r . id ) } ) ;
4658
4759 yield emit ( { type : 'phase' , phase : 'analyze' } ) ;
60+ await phasePace ( ) ;
4861 const analyst = await runAnalyst ( runId , signals , scenario . topology ) ;
62+ trackProvider ( analyst ) ;
4963 turns . push ( analyst ) ; yield emit ( { type : 'turn' , turn : analyst } ) ;
64+ await turnPace ( ) ;
5065
5166 yield emit ( { type : 'phase' , phase : 'debate' } ) ;
67+ await phasePace ( ) ;
5268 const devil = await runDevil ( runId , signals , analyst . thought ) ;
69+ trackProvider ( devil ) ;
5370 turns . push ( devil ) ; yield emit ( { type : 'turn' , turn : devil } ) ;
71+ await turnPace ( ) ;
5472
5573 yield emit ( { type : 'phase' , phase : 'strategize' } ) ;
74+ await phasePace ( ) ;
5675 const strategist = await runStrategist ( runId , signals , analyst . thought , devil . dissent || '' ) ;
76+ trackProvider ( strategist ) ;
5777 turns . push ( strategist ) ; yield emit ( { type : 'turn' , turn : strategist } ) ;
78+ await turnPace ( ) ;
5879
5980 if ( ! strategist . proposal ) {
6081 yield emit ( { type : 'error' , message : 'Strategist returned no proposal' } ) ;
@@ -66,16 +87,24 @@ export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent,
6687 const blastTurn : AgentTurn = { ...strategist , id : `t_${ nanoid ( 10 ) } ` , blastRadius : blast } ;
6788 turns . push ( blastTurn ) ;
6889 yield emit ( { type : 'blast' , score : blast } ) ;
90+ await turnPace ( ) ;
6991
7092 yield emit ( { type : 'phase' , phase : 'verify' } ) ;
93+ await phasePace ( ) ;
7194 const critic = await runCritic ( runId , action , toolCardsAsText ( ) ) ;
95+ trackProvider ( critic ) ;
7296 turns . push ( critic ) ; yield emit ( { type : 'turn' , turn : critic } ) ;
97+ await turnPace ( ) ;
7398
7499 yield emit ( { type : 'phase' , phase : 'safety' } ) ;
100+ await phasePace ( ) ;
75101 const safety = await runSafety ( runId , action , constitutionToText ( DEFAULT_CONSTITUTION ) ) ;
102+ trackProvider ( safety ) ;
76103 turns . push ( safety ) ; yield emit ( { type : 'turn' , turn : safety } ) ;
104+ await turnPace ( ) ;
77105
78106 yield emit ( { type : 'phase' , phase : 'policy_gate' } ) ;
107+ await phasePace ( ) ;
79108 const det = checkDeterministic ( action , DEFAULT_CONSTITUTION ) ;
80109 const allViolations = [
81110 ...det . violations ,
@@ -84,6 +113,7 @@ export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent,
84113 ] ;
85114 const policyAllowed = det . allowed && ( safety . policyViolations || [ ] ) . length === 0 ;
86115 yield emit ( { type : 'policy' , allowed : policyAllowed , violations : allViolations } ) ;
116+ await turnPace ( ) ;
87117
88118 if ( ! policyAllowed ) {
89119 action = {
@@ -98,31 +128,40 @@ export async function* orchestrate(scenario: Scenario): AsyncGenerator<RunEvent,
98128 }
99129
100130 const verifier = await runVerifier ( runId , action , signals ) ;
131+ trackProvider ( verifier ) ;
101132 turns . push ( verifier ) ; yield emit ( { type : 'turn' , turn : verifier } ) ;
133+ await turnPace ( ) ;
102134
103135 yield emit ( { type : 'phase' , phase : 'confidence_gate' } ) ;
136+ await phasePace ( ) ;
104137 const fused = fuseConfidence ( turns ) ;
105138 const gate = shouldAutoAct ( action , fused , blast ) ;
106139 yield emit ( { type : 'gate' , auto : gate . auto , threshold : gate . threshold , reason : gate . reason , fusedConfidence : fused } ) ;
140+ await turnPace ( ) ;
107141
108142 yield emit ( { type : 'action' , action } ) ;
109143
110144 let outcome : RunReport [ 'outcome' ] = 'in_progress' ;
111145 let actuationOk = true ;
112146 if ( gate . auto && policyAllowed ) {
113147 yield emit ( { type : 'phase' , phase : 'act' } ) ;
148+ await phasePace ( ) ;
114149 const res = await actuate ( action ) ;
115150 actuationOk = res . ok ;
116151 yield emit ( { type : 'actuated' , ok : res . ok , details : res . details , artifact : res . artifact } ) ;
117152 outcome = res . ok ? 'auto_resolved' : 'failed' ;
118153 } else {
119154 yield emit ( { type : 'phase' , phase : 'act' } ) ;
155+ await phasePace ( ) ;
120156 yield emit ( { type : 'actuated' , ok : true , details : 'Paused for human review (HITL)' } ) ;
121157 outcome = 'hitl_required' ;
122158 }
159+ await turnPace ( ) ;
123160
124161 yield emit ( { type : 'phase' , phase : 'verify_outcome' } ) ;
162+ await phasePace ( ) ;
125163 yield emit ( { type : 'phase' , phase : 'learn' } ) ;
164+ await phasePace ( ) ;
126165
127166 const finishedAt = Date . now ( ) ;
128167 const mttrSec = Math . max ( 1 , Math . round ( ( finishedAt - startedAt ) / 1000 ) ) ;
0 commit comments