1- import { Eko , Log , SimpleSseMcpClient , type LLMs , type StreamCallbackMessage } from "@jarvis-agent/core" ;
1+ import { Eko , Log , SimpleSseMcpClient , type LLMs , type StreamCallbackMessage , type AgentContext } from "@jarvis-agent/core" ;
22import { BrowserAgent , FileAgent } from "@jarvis-agent/electron" ;
33import type { EkoResult } from "@jarvis-agent/core/types" ;
44import { BrowserWindow , WebContentsView , app } from "electron" ;
55import path from "node:path" ;
6+ import { randomUUID } from "node:crypto" ;
67import { ConfigManager } from "../utils/config-manager" ;
8+ import type { HumanRequestMessage , HumanResponseMessage , HumanInteractionContext } from "../../../src/models/human-interaction" ;
79
810export class EkoService {
911 private eko : Eko | null = null ;
@@ -12,6 +14,18 @@ export class EkoService {
1214 private mcpClient ! : SimpleSseMcpClient ;
1315 private agents ! : any [ ] ;
1416
17+ // Store pending human interaction requests
18+ private pendingHumanRequests = new Map < string , {
19+ resolve : ( value : any ) => void ;
20+ reject : ( reason ?: any ) => void ;
21+ } > ( ) ;
22+
23+ // Map toolId to requestId for human interactions
24+ private toolIdToRequestId = new Map < string , string > ( ) ;
25+
26+ // Store current human_interact toolId
27+ private currentHumanInteractToolId : string | null = null ;
28+
1529 constructor ( mainWindow : BrowserWindow , detailView : WebContentsView ) {
1630 this . mainWindow = mainWindow ;
1731 this . detailView = detailView ;
@@ -32,6 +46,12 @@ export class EkoService {
3246 return Promise . resolve ( ) ;
3347 }
3448
49+ // Capture human_interact tool's toolId when tool is being used
50+ if ( message . type === 'tool_use' && message . toolName === 'human_interact' && message . toolId ) {
51+ this . currentHumanInteractToolId = message . toolId ;
52+ Log . info ( 'Captured human_interact toolId:' , message . toolId ) ;
53+ }
54+
3555 return new Promise ( ( resolve ) => {
3656 // Send stream message to renderer process via IPC
3757 this . mainWindow . webContents . send ( 'eko-stream-message' , message ) ;
@@ -72,10 +92,68 @@ export class EkoService {
7292 } else {
7393 resolve ( ) ;
7494 }
75- } )
95+ } )
96+ } ,
97+
98+ // Human interaction callbacks
99+ onHumanConfirm : async ( agentContext : AgentContext , prompt : string ) : Promise < boolean > => {
100+ const result = await this . requestHumanInteraction ( agentContext , {
101+ interactType : 'confirm' ,
102+ prompt
103+ } ) ;
104+ return Boolean ( result ) ;
105+ } ,
106+
107+ onHumanInput : async ( agentContext : AgentContext , prompt : string ) : Promise < string > => {
108+ const result = await this . requestHumanInteraction ( agentContext , {
109+ interactType : 'input' ,
110+ prompt
111+ } ) ;
112+ return String ( result ?? '' ) ;
113+ } ,
114+
115+ onHumanSelect : async (
116+ agentContext : AgentContext ,
117+ prompt : string ,
118+ options : string [ ] ,
119+ multiple : boolean
120+ ) : Promise < string [ ] > => {
121+ const result = await this . requestHumanInteraction ( agentContext , {
122+ interactType : 'select' ,
123+ prompt,
124+ selectOptions : options ,
125+ selectMultiple : multiple
126+ } ) ;
127+ return Array . isArray ( result ) ? result : [ ] ;
76128 } ,
77- onHuman : ( message : any ) => {
78- console . log ( 'EkoService human callback:' , message ) ;
129+
130+ onHumanHelp : async (
131+ agentContext : AgentContext ,
132+ helpType : 'request_login' | 'request_assistance' ,
133+ prompt : string
134+ ) : Promise < boolean > => {
135+ // Get current page information for context
136+ let context : HumanInteractionContext | undefined ;
137+ try {
138+ const url = this . detailView . webContents . getURL ( ) ;
139+ if ( url && url . startsWith ( 'http' ) ) {
140+ const hostname = new URL ( url ) . hostname ;
141+ context = {
142+ siteName : hostname ,
143+ actionUrl : url
144+ } ;
145+ }
146+ } catch ( error ) {
147+ Log . error ( 'Failed to get URL for human help context:' , error ) ;
148+ }
149+
150+ const result = await this . requestHumanInteraction ( agentContext , {
151+ interactType : 'request_help' ,
152+ prompt,
153+ helpType,
154+ context
155+ } ) ;
156+ return Boolean ( result ) ;
79157 }
80158 } ;
81159 }
@@ -142,7 +220,7 @@ export class EkoService {
142220 // Abort all running tasks before reloading
143221 if ( this . eko ) {
144222 const allTaskIds = this . eko . getAllTaskId ( ) ;
145- allTaskIds . forEach ( taskId => {
223+ allTaskIds . forEach ( ( taskId : any ) => {
146224 try {
147225 this . eko ! . abortTask ( taskId , 'config-reload' ) ;
148226 } catch ( error ) {
@@ -151,6 +229,9 @@ export class EkoService {
151229 } ) ;
152230 }
153231
232+ // Reject all pending human interactions
233+ this . rejectAllHumanRequests ( new Error ( 'EkoService configuration reloaded' ) ) ;
234+
154235 // Get new LLMs configuration
155236 const configManager = ConfigManager . getInstance ( ) ;
156237 const llms : LLMs = configManager . getLLMsConfig ( ) ;
@@ -318,17 +399,153 @@ export class EkoService {
318399 }
319400
320401 const allTaskIds = this . eko . getAllTaskId ( ) ;
321- const abortPromises = allTaskIds . map ( taskId => this . eko ! . abortTask ( taskId , 'window-closing' ) ) ;
402+ const abortPromises = allTaskIds . map ( ( taskId : any ) => this . eko ! . abortTask ( taskId , 'window-closing' ) ) ;
322403
323404 await Promise . all ( abortPromises ) ;
405+
406+ // Reject all pending human interactions (also clears toolId mappings)
407+ this . rejectAllHumanRequests ( new Error ( 'All tasks aborted' ) ) ;
408+
324409 Log . info ( 'All tasks aborted' ) ;
325410 }
326411
412+ /**
413+ * Request human interaction
414+ * Sends interaction request to frontend and waits for user response
415+ */
416+ private requestHumanInteraction (
417+ agentContext : AgentContext ,
418+ payload : Omit < HumanRequestMessage , 'type' | 'requestId' | 'timestamp' >
419+ ) : Promise < any > {
420+ const requestId = randomUUID ( ) ;
421+ const message : HumanRequestMessage = {
422+ type : 'human_interaction' ,
423+ requestId,
424+ taskId : agentContext ?. context ?. taskId ,
425+ agentName : agentContext ?. agent ?. name ,
426+ timestamp : new Date ( ) ,
427+ ...payload
428+ } ;
429+
430+ return new Promise ( ( resolve , reject ) => {
431+ // Store promise resolver/rejector
432+ this . pendingHumanRequests . set ( requestId , { resolve, reject } ) ;
433+
434+ // Map toolId to requestId for frontend response matching
435+ if ( this . currentHumanInteractToolId ) {
436+ this . toolIdToRequestId . set ( this . currentHumanInteractToolId , requestId ) ;
437+ Log . info ( 'Mapped toolId to requestId:' , {
438+ toolId : this . currentHumanInteractToolId ,
439+ requestId
440+ } ) ;
441+ // Clear after mapping
442+ this . currentHumanInteractToolId = null ;
443+ }
444+
445+ // Listen for task abort signal
446+ const controllerSignal = agentContext ?. context ?. controller ?. signal ;
447+ if ( controllerSignal ) {
448+ controllerSignal . addEventListener ( 'abort' , ( ) => {
449+ this . pendingHumanRequests . delete ( requestId ) ;
450+ reject ( new Error ( 'Task aborted during human interaction' ) ) ;
451+ } ) ;
452+ }
453+
454+ // Send request to frontend as a special message
455+ if ( ! this . mainWindow || this . mainWindow . isDestroyed ( ) ) {
456+ this . pendingHumanRequests . delete ( requestId ) ;
457+ reject ( new Error ( 'Main window destroyed, cannot request human interaction' ) ) ;
458+ return ;
459+ }
460+
461+ Log . info ( 'Requesting human interaction:' , { requestId, interactType : payload . interactType , prompt : payload . prompt } ) ;
462+ this . mainWindow . webContents . send ( 'eko-stream-message' , message ) ;
463+ } ) ;
464+ }
465+
466+ /**
467+ * Handle human response from frontend
468+ * Called via IPC when user completes interaction
469+ */
470+ public handleHumanResponse ( response : HumanResponseMessage ) : boolean {
471+ Log . info ( 'Received human response:' , response ) ;
472+
473+ // First try direct requestId match
474+ let pending = this . pendingHumanRequests . get ( response . requestId ) ;
475+ let actualRequestId = response . requestId ;
476+
477+ // If not found, try to find via toolId mapping (frontend might send toolId as requestId)
478+ if ( ! pending ) {
479+ const mappedRequestId = this . toolIdToRequestId . get ( response . requestId ) ;
480+ if ( mappedRequestId ) {
481+ pending = this . pendingHumanRequests . get ( mappedRequestId ) ;
482+ actualRequestId = mappedRequestId ;
483+ Log . info ( 'Found requestId via toolId mapping:' , {
484+ toolId : response . requestId ,
485+ actualRequestId : mappedRequestId
486+ } ) ;
487+ }
488+ }
489+
490+ if ( ! pending ) {
491+ Log . error ( `Human interaction request ${ response . requestId } not found or already processed` ) ;
492+ return false ;
493+ }
494+
495+ // Clean up both maps
496+ this . pendingHumanRequests . delete ( actualRequestId ) ;
497+ this . toolIdToRequestId . delete ( response . requestId ) ;
498+
499+ if ( response . success ) {
500+ // Resolve promise, AI continues execution
501+ pending . resolve ( response . result ) ;
502+
503+ // Send result message to frontend to update card state
504+ if ( ! this . mainWindow || this . mainWindow . isDestroyed ( ) ) {
505+ Log . warn ( 'Main window destroyed, cannot send interaction result' ) ;
506+ return true ;
507+ }
508+
509+ this . mainWindow . webContents . send ( 'eko-stream-message' , {
510+ type : 'human_interaction_result' ,
511+ requestId : response . requestId ,
512+ result : response . result ,
513+ timestamp : new Date ( )
514+ } ) ;
515+ } else {
516+ // Reject promise, AI handles error
517+ pending . reject ( new Error ( response . error || 'Human interaction cancelled' ) ) ;
518+ }
519+
520+ return true ;
521+ }
522+
523+ /**
524+ * Reject all pending human interaction requests
525+ * Used when config reloads or service shuts down
526+ */
527+ private rejectAllHumanRequests ( error : Error ) : void {
528+ if ( this . pendingHumanRequests . size === 0 ) {
529+ return ;
530+ }
531+
532+ Log . info ( `Rejecting ${ this . pendingHumanRequests . size } pending human interaction requests` ) ;
533+
534+ for ( const pending of this . pendingHumanRequests . values ( ) ) {
535+ pending . reject ( error ) ;
536+ }
537+
538+ this . pendingHumanRequests . clear ( ) ;
539+ this . toolIdToRequestId . clear ( ) ;
540+ this . currentHumanInteractToolId = null ;
541+ }
542+
327543 /**
328544 * Destroy service
329545 */
330546 destroy ( ) {
331- console . log ( 'EkoService destroyed' ) ;
547+ Log . info ( 'EkoService destroyed' ) ;
548+ this . rejectAllHumanRequests ( new Error ( 'EkoService destroyed' ) ) ;
332549 this . eko = null ;
333550 }
334551}
0 commit comments