Canceling Requests
Cancel in-progress agent requests.
Overview
Cancel requests when:
User cancels the operation
Timeout occurs
New request needs to start
User navigates away
Basic Cancellation
Using Agent
// Start a request
var sendTask = agent.SendAsync("Write a long story...");
// Cancel it
await agent.CancelAsync();
// The sendTask will throw OperationCanceledException
try
{
await sendTask;
}
catch (OperationCanceledException)
{
Debug.Log("Request was cancelled");
}With CancellationToken
CancellationTokenSource cts = new CancellationTokenSource();
// Start request with token
var task = agent.SendAsync("Long message", ct: cts.Token);
// Cancel after 5 seconds
await UniTask.Delay(5000);
cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Debug.Log("Timed out");
}Cancel Button
Simple Implementation
public class CancelableChat : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
[SerializeField] private GameObject cancelButton;
private CancellationTokenSource cts;
public async void SendMessage(string message)
{
cts = new CancellationTokenSource();
cancelButton.SetActive(true);
try
{
Response response = await agent.SendAsync(message, ct: cts.Token);
Debug.Log(response.Text);
}
catch (OperationCanceledException)
{
Debug.Log("Cancelled by user");
}
finally
{
cancelButton.SetActive(false);
cts?.Dispose();
cts = null;
}
}
public void CancelRequest()
{
cts?.Cancel();
}
}With UI Feedback
public class CancelWithFeedback : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
[SerializeField] private Button sendButton;
[SerializeField] private Button cancelButton;
[SerializeField] private TMP_Text statusText;
private CancellationTokenSource cts;
public async void SendMessage(string message)
{
cts = new CancellationTokenSource();
// Update UI
sendButton.interactable = false;
cancelButton.gameObject.SetActive(true);
statusText.text = "Sending...";
try
{
Response response = await agent.SendAsync(message, ct: cts.Token);
statusText.text = "Complete";
Debug.Log(response.Text);
}
catch (OperationCanceledException)
{
statusText.text = "Cancelled";
Debug.Log("Request cancelled");
}
catch (Exception ex)
{
statusText.text = $"Error: {ex.Message}";
Debug.LogError(ex);
}
finally
{
sendButton.interactable = true;
cancelButton.gameObject.SetActive(false);
cts?.Dispose();
cts = null;
}
}
public void Cancel()
{
cts?.Cancel();
statusText.text = "Cancelling...";
}
}Timeout
Request Timeout
public async UniTask<Response> SendWithTimeout(
string message,
int timeoutSeconds = 30)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
try
{
return await agent.SendAsync(message, ct: cts.Token);
}
catch (OperationCanceledException)
{
throw new TimeoutException($"Request timed out after {timeoutSeconds}s");
}
finally
{
cts.Dispose();
}
}Configurable Timeout
public class TimeoutManager : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
[SerializeField] private float timeoutSeconds = 30f;
public async UniTask<Response> SendWithTimeout(string message)
{
CancellationTokenSource cts = new CancellationTokenSource();
// Start timeout countdown
var timeoutTask = UniTask.Delay(
TimeSpan.FromSeconds(timeoutSeconds),
cancellationToken: cts.Token
);
// Start request
var requestTask = agent.SendAsync(message, ct: cts.Token);
// Wait for either to complete
var completedTask = await UniTask.WhenAny(requestTask, timeoutTask);
if (completedTask == 1)
{
// Timeout occurred
cts.Cancel();
throw new TimeoutException("Request timed out");
}
// Request completed
cts.Cancel(); // Cancel timeout
return await requestTask;
}
}Streaming Cancellation
Cancel Streaming
public class StreamingCancellation : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
[SerializeField] private TMP_Text chatText;
private CancellationTokenSource cts;
private StringBuilder currentResponse = new();
void Start()
{
agent.Stream = true;
agent.onTextDelta.AddListener(OnTextDelta);
}
public async void SendMessage(string message)
{
cts = new CancellationTokenSource();
currentResponse.Clear();
try
{
await agent.SendAsync(message, ct: cts.Token);
}
catch (OperationCanceledException)
{
Debug.Log("Streaming cancelled");
chatText.text += "\n[Cancelled]";
}
finally
{
cts?.Dispose();
cts = null;
}
}
void OnTextDelta(string delta)
{
currentResponse.Append(delta);
chatText.text = currentResponse.ToString();
}
public void Cancel()
{
cts?.Cancel();
}
}Graceful Streaming Stop
public async void StopStreaming()
{
if (agent.Status != AgentStatus.Responding)
{
return;
}
// Cancel the request
await agent.CancelAsync();
// Wait for cancellation to complete
await UniTask.WaitUntil(() => agent.Status == AgentStatus.Ready);
Debug.Log("Streaming stopped gracefully");
}Multiple Requests
Cancel Previous Request
public class SingleRequestManager : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
private CancellationTokenSource currentCts;
public async void SendMessage(string message)
{
// Cancel any existing request
currentCts?.Cancel();
currentCts?.Dispose();
// Create new token
currentCts = new CancellationTokenSource();
try
{
Response response = await agent.SendAsync(message, ct: currentCts.Token);
Debug.Log(response.Text);
}
catch (OperationCanceledException)
{
Debug.Log("Previous request cancelled");
}
finally
{
if (currentCts != null && !currentCts.IsCancellationRequested)
{
currentCts.Dispose();
currentCts = null;
}
}
}
void OnDestroy()
{
currentCts?.Cancel();
currentCts?.Dispose();
}
}Queue System
public class RequestQueue : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
private Queue<string> requestQueue = new();
private bool isProcessing = false;
private CancellationTokenSource cts;
public void EnqueueMessage(string message)
{
requestQueue.Enqueue(message);
if (!isProcessing)
{
ProcessQueue().Forget();
}
}
async UniTaskVoid ProcessQueue()
{
isProcessing = true;
while (requestQueue.Count > 0)
{
string message = requestQueue.Dequeue();
cts = new CancellationTokenSource();
try
{
Response response = await agent.SendAsync(message, ct: cts.Token);
Debug.Log($"Q: {message}");
Debug.Log($"A: {response.Text}\n");
}
catch (OperationCanceledException)
{
Debug.Log("Request cancelled");
break; // Stop processing queue
}
finally
{
cts?.Dispose();
cts = null;
}
}
isProcessing = false;
}
public void CancelQueue()
{
cts?.Cancel();
requestQueue.Clear();
}
}Cleanup
Proper Disposal
public class ProperCleanup : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
private CancellationTokenSource cts;
public async void SendMessage(string message)
{
cts?.Dispose(); // Dispose old token
cts = new CancellationTokenSource();
try
{
await agent.SendAsync(message, ct: cts.Token);
}
catch (OperationCanceledException)
{
Debug.Log("Cancelled");
}
finally
{
cts?.Dispose();
cts = null;
}
}
void OnDestroy()
{
// Clean up on destroy
cts?.Cancel();
cts?.Dispose();
}
void OnApplicationQuit()
{
// Clean up on quit
cts?.Cancel();
cts?.Dispose();
}
}Scene Transitions
Cancel on Scene Change
public class SceneTransitionHandler : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
private CancellationTokenSource cts;
void Start()
{
// Listen for scene change
UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
}
void OnDestroy()
{
UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
}
void OnSceneLoaded(
UnityEngine.SceneManagement.Scene scene,
UnityEngine.SceneManagement.LoadSceneMode mode)
{
// Cancel any in-progress requests
CancelCurrentRequest();
}
public async void SendMessage(string message)
{
cts = new CancellationTokenSource();
try
{
await agent.SendAsync(message, ct: cts.Token);
}
catch (OperationCanceledException)
{
Debug.Log("Cancelled due to scene change");
}
finally
{
cts?.Dispose();
cts = null;
}
}
void CancelCurrentRequest()
{
cts?.Cancel();
cts?.Dispose();
cts = null;
}
}Error Handling
Distinguish Cancellation from Errors
public async void SendWithErrorHandling(string message)
{
CancellationTokenSource cts = new CancellationTokenSource();
try
{
Response response = await agent.SendAsync(message, ct: cts.Token);
Debug.Log(response.Text);
}
catch (OperationCanceledException)
{
// User cancelled - not an error
Debug.Log("Request cancelled by user");
ShowMessage("Cancelled");
}
catch (ApiException ex)
{
// API error
Debug.LogError($"API error: {ex.Message}");
ShowError($"Error: {ex.Message}");
}
catch (Exception ex)
{
// Other errors
Debug.LogError($"Unexpected error: {ex.Message}");
ShowError("An unexpected error occurred");
}
finally
{
cts?.Dispose();
}
}Complete Example
using UnityEngine;
using UnityEngine.UI;
using Glitch9.AIDevKit.Agents;
using Cysharp.Threading.Tasks;
using TMPro;
using System;
using System.Threading;
public class CancellationExample : MonoBehaviour
{
[SerializeField] private AgentBehaviour agent;
[SerializeField] private TMP_InputField inputField;
[SerializeField] private Button sendButton;
[SerializeField] private Button cancelButton;
[SerializeField] private TMP_Text chatText;
[SerializeField] private TMP_Text statusText;
[Header("Settings")]
[SerializeField] private float timeoutSeconds = 30f;
private CancellationTokenSource cts;
private DateTime requestStartTime;
void Start()
{
agent.Stream = true;
sendButton.onClick.AddListener(OnSendClicked);
cancelButton.onClick.AddListener(OnCancelClicked);
cancelButton.gameObject.SetActive(false);
}
void Update()
{
// Update status during request
if (cts != null && !cts.IsCancellationRequested)
{
var elapsed = DateTime.Now - requestStartTime;
statusText.text = $"Responding... ({elapsed.TotalSeconds:F1}s)";
}
}
async void OnSendClicked()
{
string message = inputField.text.Trim();
if (string.IsNullOrWhiteSpace(message))
{
return;
}
// Clear input
inputField.text = "";
// Setup cancellation
cts?.Dispose();
cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
// Update UI
sendButton.interactable = false;
cancelButton.gameObject.SetActive(true);
requestStartTime = DateTime.Now;
statusText.text = "Sending...";
// Show user message
AppendMessage("You", message);
try
{
Response response = await agent.SendAsync(message, ct: cts.Token);
AppendMessage("Assistant", response.Text);
statusText.text = "Ready";
}
catch (OperationCanceledException)
{
AppendMessage("System", "[Cancelled]");
statusText.text = "Cancelled";
}
catch (Exception ex)
{
AppendMessage("System", $"Error: {ex.Message}");
statusText.text = "Error";
Debug.LogError(ex);
}
finally
{
// Cleanup
sendButton.interactable = true;
cancelButton.gameObject.SetActive(false);
cts?.Dispose();
cts = null;
}
}
void OnCancelClicked()
{
if (cts != null && !cts.IsCancellationRequested)
{
statusText.text = "Cancelling...";
cts.Cancel();
}
}
void AppendMessage(string sender, string message)
{
chatText.text += $"\n<b>{sender}:</b> {message}\n";
}
void OnDestroy()
{
cts?.Cancel();
cts?.Dispose();
}
}Next Steps
Last updated