55 type ProviderApprovalDecision ,
66 type ProviderRuntimeEvent ,
77 type ProviderSession ,
8+ type ProviderUserInputAnswers ,
89 ProviderDriverKind ,
910 ProviderInstanceId ,
1011 RuntimeRequestId ,
@@ -56,6 +57,14 @@ import {
5657 makeGrokAcpRuntime ,
5758 resolveGrokAcpBaseModelId ,
5859} from "../acp/GrokAcpSupport.ts" ;
60+ import {
61+ extractXAiAskUserQuestions ,
62+ extractXAiExitPlanMarkdown ,
63+ makeXAiAskUserQuestionResponse ,
64+ makeXAiExitPlanModeResponse ,
65+ XAiAskUserQuestionRequest ,
66+ XAiExitPlanModeRequest ,
67+ } from "../acp/XAiAcpExtension.ts" ;
5968import { type GrokAdapterShape } from "../Services/GrokAdapter.ts" ;
6069import { type EventNdjsonLogger , makeEventNdjsonLogger } from "./EventNdjsonLogger.ts" ;
6170
@@ -73,13 +82,17 @@ export interface GrokAdapterLiveOptions {
7382 readonly environment ?: NodeJS . ProcessEnv ;
7483 readonly nativeEventLogPath ?: string ;
7584 readonly nativeEventLogger ?: EventNdjsonLogger ;
76- readonly instanceId ?: typeof ProviderInstanceId . Type ;
85+ readonly instanceId ?: ProviderInstanceId ;
7786}
7887
7988interface PendingApproval {
8089 readonly decision : Deferred . Deferred < ProviderApprovalDecision > ;
8190}
8291
92+ interface PendingUserInput {
93+ readonly answers : Deferred . Deferred < ProviderUserInputAnswers > ;
94+ }
95+
8396interface GrokSessionContext {
8497 readonly threadId : ThreadId ;
8598 readonly acpSessionId : string ;
@@ -88,6 +101,7 @@ interface GrokSessionContext {
88101 readonly acp : AcpSessionRuntimeShape ;
89102 notificationFiber : Fiber . Fiber < void , never > | undefined ;
90103 readonly pendingApprovals : Map < ApprovalRequestId , PendingApproval > ;
104+ readonly pendingUserInputs : Map < ApprovalRequestId , PendingUserInput > ;
91105 turns : Array < { id : TurnId ; items : Array < unknown > } > ;
92106 lastPlanFingerprint : string | undefined ;
93107 activeTurnId : TurnId | undefined ;
@@ -105,6 +119,16 @@ function settlePendingApprovalsAsCancelled(
105119 ) ;
106120}
107121
122+ function settlePendingUserInputsAsEmptyAnswers (
123+ pendingUserInputs : ReadonlyMap < ApprovalRequestId , PendingUserInput > ,
124+ ) : Effect . Effect < void > {
125+ return Effect . forEach (
126+ Array . from ( pendingUserInputs . values ( ) ) ,
127+ ( pending ) => Deferred . succeed ( pending . answers , { } ) . pipe ( Effect . ignore ) ,
128+ { discard : true } ,
129+ ) ;
130+ }
131+
108132function isRecord ( value : unknown ) : value is Record < string , unknown > {
109133 return typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
110134}
@@ -287,6 +311,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
287311 if ( ctx . stopped ) return ;
288312 ctx . stopped = true ;
289313 yield * settlePendingApprovalsAsCancelled ( ctx . pendingApprovals ) ;
314+ yield * settlePendingUserInputsAsEmptyAnswers ( ctx . pendingUserInputs ) ;
290315 if ( ctx . notificationFiber ) {
291316 yield * Fiber . interrupt ( ctx . notificationFiber ) ;
292317 }
@@ -329,6 +354,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
329354 }
330355
331356 const pendingApprovals = new Map < ApprovalRequestId , PendingApproval > ( ) ;
357+ const pendingUserInputs = new Map < ApprovalRequestId , PendingUserInput > ( ) ;
332358 const sessionScope = yield * Scope . make ( "sequential" ) ;
333359 let sessionScopeTransferred = false ;
334360 yield * Effect . addFinalizer ( ( ) =>
@@ -363,6 +389,82 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
363389 ) ,
364390 ) ;
365391 const started = yield * Effect . gen ( function * ( ) {
392+ yield * Effect . forEach (
393+ [ "x.ai/ask_user_question" , "_x.ai/ask_user_question" ] as const ,
394+ ( method ) =>
395+ acp . handleExtRequest ( method , XAiAskUserQuestionRequest , ( params ) =>
396+ mapAcpCallbackFailure (
397+ Effect . gen ( function * ( ) {
398+ yield * logNative ( input . threadId , method , params ) ;
399+ const requestId = ApprovalRequestId . make ( yield * randomUUIDv4 ) ;
400+ const runtimeRequestId = RuntimeRequestId . make ( requestId ) ;
401+ const answers = yield * Deferred . make < ProviderUserInputAnswers > ( ) ;
402+ pendingUserInputs . set ( requestId , { answers } ) ;
403+ yield * offerRuntimeEvent ( {
404+ type : "user-input.requested" ,
405+ ...( yield * makeEventStamp ( ) ) ,
406+ provider : PROVIDER ,
407+ threadId : input . threadId ,
408+ turnId : sessions . get ( input . threadId ) ?. activeTurnId ,
409+ requestId : runtimeRequestId ,
410+ payload : { questions : extractXAiAskUserQuestions ( params ) } ,
411+ raw : {
412+ source : "acp.grok.extension" ,
413+ method,
414+ payload : params ,
415+ } ,
416+ } ) ;
417+ const resolved = yield * Deferred . await ( answers ) ;
418+ pendingUserInputs . delete ( requestId ) ;
419+ yield * offerRuntimeEvent ( {
420+ type : "user-input.resolved" ,
421+ ...( yield * makeEventStamp ( ) ) ,
422+ provider : PROVIDER ,
423+ threadId : input . threadId ,
424+ turnId : sessions . get ( input . threadId ) ?. activeTurnId ,
425+ requestId : runtimeRequestId ,
426+ payload : { answers : resolved } ,
427+ raw : {
428+ source : "acp.grok.extension" ,
429+ method,
430+ payload : params ,
431+ } ,
432+ } ) ;
433+ return makeXAiAskUserQuestionResponse ( resolved ) ;
434+ } ) ,
435+ ) ,
436+ ) ,
437+ { discard : true } ,
438+ ) ;
439+ yield * Effect . forEach (
440+ [ "x.ai/exit_plan_mode" , "_x.ai/exit_plan_mode" ] as const ,
441+ ( method ) =>
442+ acp . handleExtRequest ( method , XAiExitPlanModeRequest , ( params ) =>
443+ mapAcpCallbackFailure (
444+ Effect . gen ( function * ( ) {
445+ yield * logNative ( input . threadId , method , params ) ;
446+ const planMarkdown = extractXAiExitPlanMarkdown ( params ) ?. trim ( ) ;
447+ if ( planMarkdown ) {
448+ yield * offerRuntimeEvent ( {
449+ type : "turn.proposed.completed" ,
450+ ...( yield * makeEventStamp ( ) ) ,
451+ provider : PROVIDER ,
452+ threadId : input . threadId ,
453+ turnId : sessions . get ( input . threadId ) ?. activeTurnId ,
454+ payload : { planMarkdown } ,
455+ raw : {
456+ source : "acp.grok.extension" ,
457+ method,
458+ payload : params ,
459+ } ,
460+ } ) ;
461+ }
462+ return makeXAiExitPlanModeResponse ( ) ;
463+ } ) ,
464+ ) ,
465+ ) ,
466+ { discard : true } ,
467+ ) ;
366468 yield * acp . handleRequestPermission ( ( params ) =>
367469 mapAcpCallbackFailure (
368470 Effect . gen ( function * ( ) {
@@ -470,6 +572,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
470572 acp,
471573 notificationFiber : undefined ,
472574 pendingApprovals,
575+ pendingUserInputs,
473576 turns : [ ] ,
474577 lastPlanFingerprint : undefined ,
475578 activeTurnId : undefined ,
@@ -733,6 +836,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
733836 Effect . gen ( function * ( ) {
734837 const ctx = yield * requireSession ( threadId ) ;
735838 yield * settlePendingApprovalsAsCancelled ( ctx . pendingApprovals ) ;
839+ yield * settlePendingUserInputsAsEmptyAnswers ( ctx . pendingUserInputs ) ;
736840 yield * Effect . ignore (
737841 ctx . acp . cancel . pipe (
738842 Effect . mapError ( ( error ) =>
@@ -760,14 +864,22 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
760864 yield * Deferred . succeed ( pending . decision , decision ) ;
761865 } ) ;
762866
763- const respondToUserInput : GrokAdapterShape [ "respondToUserInput" ] = ( threadId , requestId ) =>
867+ const respondToUserInput : GrokAdapterShape [ "respondToUserInput" ] = (
868+ threadId ,
869+ requestId ,
870+ answers ,
871+ ) =>
764872 Effect . gen ( function * ( ) {
765- yield * requireSession ( threadId ) ;
766- return yield * new ProviderAdapterRequestError ( {
767- provider : PROVIDER ,
768- method : "user-input/respond" ,
769- detail : `Grok has no pending user-input request: ${ requestId } ` ,
770- } ) ;
873+ const ctx = yield * requireSession ( threadId ) ;
874+ const pending = ctx . pendingUserInputs . get ( requestId ) ;
875+ if ( ! pending ) {
876+ return yield * new ProviderAdapterRequestError ( {
877+ provider : PROVIDER ,
878+ method : "_x.ai/ask_user_question" ,
879+ detail : `Unknown pending user-input request: ${ requestId } ` ,
880+ } ) ;
881+ }
882+ yield * Deferred . succeed ( pending . answers , answers ) ;
771883 } ) ;
772884
773885 const readThread : GrokAdapterShape [ "readThread" ] = ( threadId ) =>
0 commit comments