@@ -2,15 +2,34 @@ import StorageUtils from '../storage';
22import { ToolCallRequest , ToolCallOutput , ToolCallParameters } from '../types' ;
33import { AgentTool } from './agent_tool' ;
44
5+ // Import the HTML content as a raw string
6+ import iframeHTMLContent from '../../assets/iframe_sandbox.html?raw' ;
7+
8+ interface IframeMessage {
9+ call_id : string ;
10+ output ?: string ;
11+ error ?: string ;
12+ command ?: 'executeCode' | 'iframeReady' ;
13+ code ?: string ;
14+ }
15+
516export class JSReplAgentTool extends AgentTool {
617 private static readonly ID = 'javascript_interpreter' ;
7- private fakeLogger : FakeConsoleLog ;
18+ private iframe : HTMLIFrameElement | null = null ;
19+ private iframeReadyPromise : Promise < void > | null = null ;
20+ private resolveIframeReady : ( ( ) => void ) | null = null ;
21+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22+ private rejectIframeReady : ( ( reason ?: any ) => void ) | null = null ;
23+ private pendingCalls = new Map < string , ( output : ToolCallOutput ) => void > ( ) ;
24+ private messageHandler :
25+ | ( ( event : MessageEvent < IframeMessage > ) => void )
26+ | null = null ;
827
928 constructor ( ) {
1029 super (
1130 JSReplAgentTool . ID ,
1231 ( ) => StorageUtils . getConfig ( ) . jsInterpreterToolUse ,
13- 'Executes JavaScript code in the browser console . The code should be self-contained valid javascript. You can use console.log(variable) to print out intermediate values.' ,
32+ 'Executes JavaScript code in a sandboxed iframe . The code should be self-contained valid javascript. You can use console.log(variable) to print out intermediate values, which will be captured .' ,
1433 {
1534 type : 'object' ,
1635 properties : {
@@ -22,53 +41,134 @@ export class JSReplAgentTool extends AgentTool {
2241 required : [ 'code' ] ,
2342 } as ToolCallParameters
2443 ) ;
25- this . fakeLogger = new FakeConsoleLog ( ) ;
44+ this . initIframe ( ) ;
2645 }
2746
28- _process ( tc : ToolCallRequest ) : ToolCallOutput {
29- const args = JSON . parse ( tc . function . arguments ) ;
47+ private initIframe ( ) : void {
48+ if ( typeof window === 'undefined' || typeof document === 'undefined' ) {
49+ console . warn (
50+ 'JSReplAgentTool: Not in a browser environment, iframe will not be created.'
51+ ) ;
52+ return ;
53+ }
3054
31- // Redirect console.log which agent will use to
32- // the fake logger so that later we can get the content
33- const originalConsoleLog = console . log ;
34- console . log = this . fakeLogger . log ;
55+ this . iframeReadyPromise = new Promise < void > ( ( resolve , reject ) => {
56+ this . resolveIframeReady = resolve ;
57+ this . rejectIframeReady = reject ;
58+ } ) ;
3559
36- let result = '' ;
37- try {
38- // Evaluate the provided agent code
39- result = eval ( args . code ) ;
40- if ( result ) {
41- result = JSON . stringify ( result , null , 2 ) ;
42- } else {
43- result = '' ;
60+ this . messageHandler = ( event : MessageEvent < IframeMessage > ) => {
61+ if (
62+ ! event . data ||
63+ ! this . iframe ||
64+ ! this . iframe . contentWindow ||
65+ event . source !== this . iframe . contentWindow
66+ ) {
67+ return ;
4468 }
45- } catch ( err ) {
46- result = String ( err ) ;
47- }
4869
49- console . log = originalConsoleLog ;
50- result = this . fakeLogger . content + result ;
70+ const { command, call_id, output, error } = event . data ;
71+ if ( command === 'iframeReady' && call_id === 'initial_ready' ) {
72+ if ( this . resolveIframeReady ) {
73+ this . resolveIframeReady ( ) ;
74+ this . resolveIframeReady = null ;
75+ this . rejectIframeReady = null ;
76+ }
77+ return ;
78+ }
79+ if ( typeof call_id !== 'string' ) {
80+ return ;
81+ }
82+ if ( this . pendingCalls . has ( call_id ) ) {
83+ const callback = this . pendingCalls . get ( call_id ) ! ;
84+ callback ( {
85+ type : 'function_call_output' ,
86+ call_id : call_id ,
87+ output : error ? `Error: ${ error } ` : ( output ?? '' ) ,
88+ } as ToolCallOutput ) ;
89+ this . pendingCalls . delete ( call_id ) ;
90+ }
91+ } ;
92+ window . addEventListener ( 'message' , this . messageHandler ) ;
5193
52- this . fakeLogger . clear ( ) ;
94+ this . iframe = document . createElement ( 'iframe' ) ;
95+ this . iframe . style . display = 'none' ;
96+ this . iframe . sandbox . add ( 'allow-scripts' ) ;
5397
54- return { call_id : tc . call_id , output : result } as ToolCallOutput ;
55- }
56- }
98+ // Use srcdoc with the imported HTML content
99+ this . iframe . srcdoc = iframeHTMLContent ;
57100
58- class FakeConsoleLog {
59- private _content : string = '' ;
101+ document . body . appendChild ( this . iframe ) ;
60102
61- public get content ( ) : string {
62- return this . _content ;
103+ setTimeout ( ( ) => {
104+ if ( this . rejectIframeReady ) {
105+ this . rejectIframeReady ( new Error ( 'Iframe readiness timeout' ) ) ;
106+ this . resolveIframeReady = null ;
107+ this . rejectIframeReady = null ;
108+ }
109+ } , 5000 ) ;
63110 }
64111
65- // Use an arrow function for log to correctly bind 'this'
66- public log = ( ...args : any [ ] ) : void => {
67- // Convert arguments to strings and join them.
68- this . _content += args . map ( ( arg ) => String ( arg ) ) . join ( ' ' ) + '\n' ;
69- } ;
112+ async _process ( tc : ToolCallRequest ) : Promise < ToolCallOutput > {
113+ let error = null ;
114+ if (
115+ typeof window === 'undefined' ||
116+ ! this . iframe ||
117+ ! this . iframe . contentWindow ||
118+ ! this . iframeReadyPromise
119+ ) {
120+ error =
121+ 'Error: JavaScript interpreter is not available or iframe not ready.' ;
122+ }
123+
124+ try {
125+ await this . iframeReadyPromise ;
126+ } catch ( e ) {
127+ error = `Error: Iframe for JavaScript interpreter failed to initialize. ${ ( e as Error ) . message } ` ;
128+ }
129+
130+ let args ;
131+ try {
132+ args = JSON . parse ( tc . function . arguments ) ;
133+ } catch ( e ) {
134+ error = `Error: Could not parse arguments for tool call. ${ ( e as Error ) . message } ` ;
135+ }
136+
137+ const codeToExecute = args . code ;
138+ if ( typeof codeToExecute !== 'string' ) {
139+ error = 'Error: "code" argument must be a string.' ;
140+ }
141+
142+ if ( error ) {
143+ return {
144+ type : 'function_call_output' ,
145+ call_id : tc . call_id ,
146+ output : error ,
147+ } as ToolCallOutput ;
148+ }
149+
150+ return new Promise < ToolCallOutput > ( ( resolve ) => {
151+ this . pendingCalls . set ( tc . call_id , resolve ) ;
152+ const message : IframeMessage = {
153+ command : 'executeCode' ,
154+ code : codeToExecute ,
155+ call_id : tc . call_id ,
156+ } ;
157+ this . iframe ! . contentWindow ! . postMessage ( message , '*' ) ;
158+ } ) ;
159+ }
70160
71- public clear = ( ) : void => {
72- this . _content = '' ;
73- } ;
161+ // public dispose(): void {
162+ // if (this.iframe) {
163+ // document.body.removeChild(this.iframe);
164+ // this.iframe = null;
165+ // }
166+ // if (this.messageHandler) {
167+ // window.removeEventListener('message', this.messageHandler);
168+ // this.messageHandler = null;
169+ // }
170+ // this.pendingCalls.clear();
171+ // this.resolveIframeReady = null;
172+ // this.rejectIframeReady = null;
173+ // }
74174}
0 commit comments