@@ -88,13 +88,21 @@ public class Chat {
8888 */
8989 @ Setter
9090 private boolean functionsEnabled = true ;
91+
92+ /**
93+ * Flag to enable or disable server-side tools (e.g., Google Search).
94+ */
95+ @ Setter
96+ private boolean serverToolsEnabled = false ;
9197
9298 private volatile boolean isProcessing = false ;
9399 private volatile boolean shutdown = false ;
94100 private Date startTime = new Date ();
95101
96102 private final AtomicLong messageCounter = new AtomicLong (0 );
97103
104+ private volatile Thread processingThread ;
105+
98106 /**
99107 * Resets the sequential message counter to a specific value.
100108 * Useful when restoring a session from persistent storage.
@@ -131,11 +139,23 @@ public Chat(
131139 public void shutdown () {
132140 log .info ("Shutting down Chat for session {}" , config .getSessionId ());
133141 this .shutdown = true ;
142+ kill ();
134143 if (executor != null && !executor .isShutdown ()) {
135144 executor .shutdown ();
136145 }
137146 }
138147
148+ /**
149+ * Interrupts the current processing thread, effectively cancelling any ongoing API call or tool execution.
150+ */
151+ public void kill () {
152+ Thread t = processingThread ;
153+ if (t != null ) {
154+ log .info ("Killing active processing thread: {}" , t .getName ());
155+ t .interrupt ();
156+ }
157+ }
158+
139159 /**
140160 * Adds a listener to be notified of changes in the conversation context.
141161 *
@@ -203,12 +223,17 @@ public void sendText(String message) {
203223 }
204224
205225 private ChatMessage buildChatMessage (Content content , GenerateContentResponseUsageMetadata usage , GroundingMetadata grounding ) {
226+ return buildChatMessage (content , usage , grounding , false );
227+ }
228+
229+ private ChatMessage buildChatMessage (Content content , GenerateContentResponseUsageMetadata usage , GroundingMetadata grounding , boolean toolFeedback ) {
206230 return ChatMessage .builder ()
207231 .sequentialId (messageCounter .incrementAndGet ())
208232 .modelId (config .getApi ().getModelId ())
209233 .content (content )
210234 .usageMetadata (usage )
211235 .groundingMetadata (grounding )
236+ .toolFeedback (toolFeedback )
212237 .build ();
213238 }
214239
@@ -223,6 +248,7 @@ public void sendContent(Content content) {
223248 return ;
224249 }
225250 isProcessing = true ;
251+ processingThread = Thread .currentThread ();
226252 statusManager .recordUserInputTime ();
227253 statusManager .setStatus (ChatStatus .API_CALL_IN_PROGRESS );
228254 try {
@@ -231,12 +257,17 @@ public void sendContent(Content content) {
231257
232258 processModelResponseLoop ();
233259 } catch (Exception e ) {
234- log .error ("An unhandled exception occurred during the processing loop." , e );
260+ if (Thread .interrupted () || e .getCause () instanceof InterruptedException ) {
261+ log .info ("Processing loop interrupted/killed." );
262+ } else {
263+ log .error ("An unhandled exception occurred during the processing loop." , e );
264+ }
235265 // If an exception bubbles all the way up, it's a critical failure.
236266 // The MAX_RETRIES_REACHED status would have already been set inside the loop.
237267 // We don't want the finally block to override it.
238268 } finally {
239269 isProcessing = false ;
270+ processingThread = null ;
240271 // Only reset to IDLE if we are not in a terminal error state.
241272 if (statusManager .getCurrentStatus () != ChatStatus .MAX_RETRIES_REACHED ) {
242273 statusManager .setStatus (ChatStatus .IDLE_WAITING_FOR_USER );
@@ -246,14 +277,18 @@ public void sendContent(Content content) {
246277
247278 private void processModelResponseLoop () {
248279 while (true ) {
249- if (shutdown ) {
250- log .info ("Shutdown flag detected. Breaking processing loop for session {}." , config .getSessionId ());
280+ if (shutdown || Thread . interrupted () ) {
281+ log .info ("Shutdown or interrupt flag detected. Breaking processing loop for session {}." , config .getSessionId ());
251282 break ;
252283 }
253284
254285 List <Content > apiContext = buildApiContext (contextManager .getContext ());
255286 GenerateContentResponse resp = sendToModelWithRetry (apiContext );
256287
288+ if (resp == null ) {
289+ break ; // Interrupted
290+ }
291+
257292 if (resp .candidates () == null || !resp .candidates ().isPresent () || resp .candidates ().get ().isEmpty ()) {
258293 log .warn ("Received response with no candidates. Possibly due to safety filters. Breaking loop." );
259294 Content emptyContent = Content .builder ().role ("model" ).parts (Part .fromText ("[No response from model]" )).build ();
@@ -302,7 +337,7 @@ private boolean processAndReloopForFunctionCalls(ChatMessage modelMessageWithCal
302337 String feedbackText = "User Feedback: " + feedback ;
303338
304339 Content feedbackContent = Content .builder ().role ("user" ).parts (Part .fromText (feedbackText )).build ();
305- ChatMessage feedbackMessage = buildChatMessage (feedbackContent , null , null );
340+ ChatMessage feedbackMessage = buildChatMessage (feedbackContent , null , null , true );
306341 contextManager .add (feedbackMessage );
307342 return false ;
308343 }
@@ -367,7 +402,7 @@ private boolean processAndReloopForFunctionCalls(ChatMessage modelMessageWithCal
367402 userFeedbackParts .addAll (extraUserParts );
368403
369404 Content feedbackContent = Content .builder ().role ("user" ).parts (userFeedbackParts ).build ();
370- ChatMessage feedbackMessage = buildChatMessage (feedbackContent , null , null );
405+ ChatMessage feedbackMessage = buildChatMessage (feedbackContent , null , null , true );
371406 feedbackMessage .setDependencies (userFeedbackDependencies ); // Set dependencies after creation
372407 messagesToAdd .add (feedbackMessage );
373408 }
@@ -386,6 +421,9 @@ private GenerateContentResponse sendToModelWithRetry(List<Content> context) {
386421 long backoffAmount = 0 ;
387422
388423 for (int attempt = 0 ; attempt < maxRetries ; attempt ++) {
424+ if (Thread .interrupted ()) {
425+ return null ;
426+ }
389427 Client client = getGoogleGenAIClient ();
390428 try {
391429 statusManager .setStatus (ChatStatus .API_CALL_IN_PROGRESS );
@@ -427,6 +465,9 @@ private GenerateContentResponse sendToModelWithRetry(List<Content> context) {
427465
428466 return ret ;
429467 } catch (Exception e ) {
468+ if (Thread .interrupted () || e .getCause () instanceof InterruptedException ) {
469+ return null ;
470+ }
430471 log .warn ("Api Error on attempt {}: {}" , attempt , e .toString ());
431472 String apiKey = client .apiKey ();
432473 String apiKeyLast5 = StringUtils .right (apiKey , 5 );
@@ -446,7 +487,7 @@ private GenerateContentResponse sendToModelWithRetry(List<Content> context) {
446487 Thread .sleep (backoffAmount );
447488 } catch (InterruptedException ie ) {
448489 Thread .currentThread ().interrupt ();
449- throw new RuntimeException ( "Chat was interrupted during retry delay." , ie ) ;
490+ return null ;
450491 }
451492 } else {
452493 log .error ("Unknown error from Google's servers" , e );
0 commit comments