diff --git a/samples/AgenTerra.Sample/ReasoningToolSample.cs b/samples/AgenTerra.Sample/ReasoningToolSample.cs
index 5f08093..b8b0baa 100644
--- a/samples/AgenTerra.Sample/ReasoningToolSample.cs
+++ b/samples/AgenTerra.Sample/ReasoningToolSample.cs
@@ -16,111 +16,111 @@ public static async Task RunAsync()
Console.WriteLine("=== Fox, Chicken, and Grain River Crossing Puzzle ===");
Console.WriteLine();
- var reasoningTool = new ReasoningTool();
+ using var reasoningTool = new ReasoningTool();
var sessionId = Guid.NewGuid().ToString();
// Step 1: Think - Initial State Analysis
var response1 = await reasoningTool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: "Initial State Analysis",
- Thought: "Man, fox, chicken, grain on left bank. Goal: all on right bank. Boat holds man + one item.",
- Action: "Identify constraints",
- Confidence: 0.9
+ sessionId,
+ "Initial State Analysis",
+ "Man, fox, chicken, grain on left bank. Goal: all on right bank. Boat holds man + one item.",
+ "Identify constraints",
+ 0.9
));
Console.WriteLine(response1);
// Step 2: Analyze - Constraint Analysis
var response2 = await reasoningTool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: "Constraint Analysis",
- Result: "Fox eats chicken if alone. Chicken eats grain if alone.",
- Analysis: "Must never leave fox+chicken or chicken+grain together without the man present.",
- NextAction: NextAction.Continue,
- Confidence: 0.95
+ sessionId,
+ "Constraint Analysis",
+ "Fox eats chicken if alone. Chicken eats grain if alone.",
+ "Must never leave fox+chicken or chicken+grain together without the man present.",
+ NextAction.Continue,
+ 0.95
));
Console.WriteLine(response2);
// Step 3: Think - First Move Strategy
var response3 = await reasoningTool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: "First Move Strategy",
- Thought: "Chicken is the conflict point. If we take fox first, chicken eats grain. If we take grain first, fox eats chicken.",
- Action: "Take chicken across first",
- Confidence: 0.85
+ sessionId,
+ "First Move Strategy",
+ "Chicken is the conflict point. If we take fox first, chicken eats grain. If we take grain first, fox eats chicken.",
+ "Take chicken across first",
+ 0.85
));
Console.WriteLine(response3);
// Step 4: Analyze - After First Move
var response4 = await reasoningTool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: "After First Move",
- Result: "Left bank: fox, grain. Right bank: chicken. Boat with man on left.",
- Analysis: "Fox and grain safe together. Can now move either fox or grain.",
- NextAction: NextAction.Continue,
- Confidence: 0.9
+ sessionId,
+ "After First Move",
+ "Left bank: fox, grain. Right bank: chicken. Boat with man on left.",
+ "Fox and grain safe together. Can now move either fox or grain.",
+ NextAction.Continue,
+ 0.9
));
Console.WriteLine(response4);
// Step 5: Think - Second Move
var response5 = await reasoningTool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: "Second Move",
- Thought: "Take fox across. But if we leave fox with chicken, fox eats chicken.",
- Action: "Take fox across, bring chicken back",
- Confidence: 0.8
+ sessionId,
+ "Second Move",
+ "Take fox across. But if we leave fox with chicken, fox eats chicken.",
+ "Take fox across, bring chicken back",
+ 0.8
));
Console.WriteLine(response5);
// Step 6: Analyze - After Second Move
var response6 = await reasoningTool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: "After Second Move",
- Result: "Left bank: chicken, grain. Right bank: fox. Boat with man on left.",
- Analysis: "Fox is safe alone. Now need to get grain across without leaving it with chicken.",
- NextAction: NextAction.Continue,
- Confidence: 0.9
+ sessionId,
+ "After Second Move",
+ "Left bank: chicken, grain. Right bank: fox. Boat with man on left.",
+ "Fox is safe alone. Now need to get grain across without leaving it with chicken.",
+ NextAction.Continue,
+ 0.9
));
Console.WriteLine(response6);
// Step 7: Think - Third Move
var response7 = await reasoningTool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: "Third Move",
- Thought: "Take grain across, leave fox and grain together (safe).",
- Action: "Take grain across",
- Confidence: 0.9
+ sessionId,
+ "Third Move",
+ "Take grain across, leave fox and grain together (safe).",
+ "Take grain across",
+ 0.9
));
Console.WriteLine(response7);
// Step 8: Analyze - After Third Move
var response8 = await reasoningTool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: "After Third Move",
- Result: "Left bank: chicken. Right bank: fox, grain. Boat with man on right.",
- Analysis: "Fox and grain are safe together. Only chicken remains on left bank.",
- NextAction: NextAction.Continue,
- Confidence: 0.95
+ sessionId,
+ "After Third Move",
+ "Left bank: chicken. Right bank: fox, grain. Boat with man on right.",
+ "Fox and grain are safe together. Only chicken remains on left bank.",
+ NextAction.Continue,
+ 0.95
));
Console.WriteLine(response8);
// Step 9: Think - Final Move
var response9 = await reasoningTool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: "Final Move",
- Thought: "Go back empty, get chicken.",
- Action: "Take chicken across",
- Confidence: 1.0
+ sessionId,
+ "Final Move",
+ "Go back empty, get chicken.",
+ "Take chicken across",
+ 1.0
));
Console.WriteLine(response9);
// Step 10: Analyze - Final State
var response10 = await reasoningTool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: "Final State",
- Result: "Left bank: empty. Right bank: fox, chicken, grain, man.",
- Analysis: "All items successfully transported. Puzzle solved!",
- NextAction: NextAction.FinalAnswer,
- Confidence: 1.0
+ sessionId,
+ "Final State",
+ "Left bank: empty. Right bank: fox, chicken, grain, man.",
+ "All items successfully transported. Puzzle solved!",
+ NextAction.FinalAnswer,
+ 1.0
));
Console.WriteLine(response10);
@@ -131,7 +131,7 @@ public static async Task RunAsync()
{
var step = history[i];
Console.WriteLine($"Step {i + 1}: [{step.Type.ToUpper()}] {step.Title}");
- Console.WriteLine($"Confidence: {step.Confidence:F2}");
+ Console.WriteLine($" {step.Confidence:F2}");
Console.WriteLine($"Timestamp: {step.Timestamp:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine(step.Content);
Console.WriteLine();
diff --git a/src/AgenTerra.Core/AgenTerra.Core.csproj b/src/AgenTerra.Core/AgenTerra.Core.csproj
index 51e5590..8691903 100644
--- a/src/AgenTerra.Core/AgenTerra.Core.csproj
+++ b/src/AgenTerra.Core/AgenTerra.Core.csproj
@@ -7,6 +7,7 @@
+
diff --git a/src/AgenTerra.Core/Reasoning/IReasoningTool.cs b/src/AgenTerra.Core/Reasoning/IReasoningTool.cs
index 6ff81db..15a7e20 100644
--- a/src/AgenTerra.Core/Reasoning/IReasoningTool.cs
+++ b/src/AgenTerra.Core/Reasoning/IReasoningTool.cs
@@ -9,21 +9,28 @@ public interface IReasoningTool
/// Records a thinking step with a thought and optional action.
///
/// The thinking input containing the session ID, title, thought, and optional action.
+ /// Cancellation token.
/// A formatted string response suitable for LLM consumption.
- Task ThinkAsync(ThinkInput input);
+ Task ThinkAsync(ThinkInput input, CancellationToken cancellationToken = default);
///
/// Records an analysis step with a result and analysis of the next action.
///
/// The analysis input containing the session ID, title, result, analysis, and next action.
+ /// Cancellation token.
/// A formatted string response suitable for LLM consumption.
- Task AnalyzeAsync(AnalyzeInput input);
+ Task AnalyzeAsync(AnalyzeInput input, CancellationToken cancellationToken = default);
///
/// Retrieves the complete reasoning history for a specific session.
///
/// The unique identifier of the session.
/// An immutable list of reasoning steps for the session.
+ ///
+ /// Warning: This method blocks synchronously to acquire a lock.
+ /// In ASP.NET or UI contexts with a synchronization context, this may cause deadlocks.
+ /// This method is synchronous to maintain backward compatibility.
+ ///
IReadOnlyList GetReasoningHistory(string sessionId);
}
diff --git a/src/AgenTerra.Core/Reasoning/Models/AnalyzeInput.cs b/src/AgenTerra.Core/Reasoning/Models/AnalyzeInput.cs
index e99f8e7..2614449 100644
--- a/src/AgenTerra.Core/Reasoning/Models/AnalyzeInput.cs
+++ b/src/AgenTerra.Core/Reasoning/Models/AnalyzeInput.cs
@@ -1,21 +1,53 @@
-using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
namespace AgenTerra.Core.Reasoning;
///
/// Represents input for an analysis step in the reasoning process.
///
-/// The unique identifier of the reasoning session.
-/// A brief title describing this analysis step.
-/// The result or observation being analyzed.
-/// The analysis of the result.
-/// The recommended next action in the reasoning process.
-/// Confidence level in this analysis (0.0 to 1.0, default 0.8).
-public record AnalyzeInput(
- [Required] string SessionId,
- [Required] string Title,
- [Required] string Result,
- [Required] string Analysis,
- NextAction NextAction = NextAction.Continue,
- double Confidence = 0.8
-);
+public record AnalyzeInput
+{
+ ///
+ /// Gets the unique identifier of the reasoning session.
+ ///
+ public required string SessionId { get; init; }
+
+ ///
+ /// Gets a brief title describing this analysis step.
+ ///
+ public required string Title { get; init; }
+
+ ///
+ /// Gets the result or observation being analyzed.
+ ///
+ public required string Result { get; init; }
+
+ ///
+ /// Gets the analysis of the result.
+ ///
+ public required string Analysis { get; init; }
+
+ ///
+ /// Gets the recommended next action in the reasoning process.
+ ///
+ public NextAction NextAction { get; init; } = NextAction.Continue;
+
+ ///
+ /// Gets the confidence level in this analysis (0.0 to 1.0).
+ ///
+ public double Confidence { get; init; } = 0.8;
+
+ ///
+ /// Initializes a new instance of the AnalyzeInput record with positional parameters (for backward compatibility).
+ ///
+ [SetsRequiredMembers]
+ public AnalyzeInput(string sessionId, string title, string result, string analysis, NextAction nextAction = NextAction.Continue, double confidence = 0.8)
+ {
+ SessionId = sessionId;
+ Title = title;
+ Result = result;
+ Analysis = analysis;
+ NextAction = nextAction;
+ Confidence = confidence;
+ }
+}
diff --git a/src/AgenTerra.Core/Reasoning/Models/ReasoningException.cs b/src/AgenTerra.Core/Reasoning/Models/ReasoningException.cs
new file mode 100644
index 0000000..799e27a
--- /dev/null
+++ b/src/AgenTerra.Core/Reasoning/Models/ReasoningException.cs
@@ -0,0 +1,21 @@
+namespace AgenTerra.Core.Reasoning;
+
+///
+/// Exception thrown when reasoning operations fail.
+///
+public class ReasoningException : Exception
+{
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The message that describes the error.
+ public ReasoningException(string message) : base(message) { }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message and a reference to the inner exception.
+ ///
+ /// The message that describes the error.
+ /// The exception that is the cause of the current exception.
+ public ReasoningException(string message, Exception innerException)
+ : base(message, innerException) { }
+}
diff --git a/src/AgenTerra.Core/Reasoning/Models/ReasoningStep.cs b/src/AgenTerra.Core/Reasoning/Models/ReasoningStep.cs
index 1f8d6c7..f6b8396 100644
--- a/src/AgenTerra.Core/Reasoning/Models/ReasoningStep.cs
+++ b/src/AgenTerra.Core/Reasoning/Models/ReasoningStep.cs
@@ -3,15 +3,30 @@ namespace AgenTerra.Core.Reasoning;
///
/// Represents a single step in the reasoning process.
///
-/// The type of step ("think" or "analyze").
-/// A brief title describing this step.
-/// The content of the reasoning step.
-/// Confidence level in this step (0.0 to 1.0).
-/// When this step was recorded.
-public record ReasoningStep(
- string Type,
- string Title,
- string Content,
- double Confidence,
- DateTime Timestamp
-);
+public record ReasoningStep
+{
+ ///
+ /// Gets the type of step ("think" or "analyze").
+ ///
+ public required string Type { get; init; }
+
+ ///
+ /// Gets a brief title describing this step.
+ ///
+ public required string Title { get; init; }
+
+ ///
+ /// Gets the content of the reasoning step.
+ ///
+ public required string Content { get; init; }
+
+ ///
+ /// Gets the confidence level in this step (0.0 to 1.0).
+ ///
+ public required double Confidence { get; init; }
+
+ ///
+ /// Gets the timestamp when this step was recorded.
+ ///
+ public required DateTime Timestamp { get; init; }
+}
diff --git a/src/AgenTerra.Core/Reasoning/Models/ThinkInput.cs b/src/AgenTerra.Core/Reasoning/Models/ThinkInput.cs
index 018f5a5..591fc8e 100644
--- a/src/AgenTerra.Core/Reasoning/Models/ThinkInput.cs
+++ b/src/AgenTerra.Core/Reasoning/Models/ThinkInput.cs
@@ -1,19 +1,47 @@
-using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
namespace AgenTerra.Core.Reasoning;
///
/// Represents input for a thinking step in the reasoning process.
///
-/// The unique identifier of the reasoning session.
-/// A brief title describing this thinking step.
-/// The main thought or reasoning content.
-/// An optional action to take based on this thought.
-/// Confidence level in this thought (0.0 to 1.0, default 0.8).
-public record ThinkInput(
- [Required] string SessionId,
- [Required] string Title,
- [Required] string Thought,
- string? Action = null,
- double Confidence = 0.8
-);
+public record ThinkInput
+{
+ ///
+ /// Gets the unique identifier of the reasoning session.
+ ///
+ public required string SessionId { get; init; }
+
+ ///
+ /// Gets a brief title describing this thinking step.
+ ///
+ public required string Title { get; init; }
+
+ ///
+ /// Gets the main thought or reasoning content.
+ ///
+ public required string Thought { get; init; }
+
+ ///
+ /// Gets an optional action to take based on this thought.
+ ///
+ public string? Action { get; init; }
+
+ ///
+ /// Gets the confidence level in this thought (0.0 to 1.0).
+ ///
+ public double Confidence { get; init; } = 0.8;
+
+ ///
+ /// Initializes a new instance of the ThinkInput record with positional parameters (for backward compatibility).
+ ///
+ [SetsRequiredMembers]
+ public ThinkInput(string sessionId, string title, string thought, string? action = null, double confidence = 0.8)
+ {
+ SessionId = sessionId;
+ Title = title;
+ Thought = thought;
+ Action = action;
+ Confidence = confidence;
+ }
+}
diff --git a/src/AgenTerra.Core/Reasoning/ReasoningTool.cs b/src/AgenTerra.Core/Reasoning/ReasoningTool.cs
index 0338bfb..d0b2a3a 100644
--- a/src/AgenTerra.Core/Reasoning/ReasoningTool.cs
+++ b/src/AgenTerra.Core/Reasoning/ReasoningTool.cs
@@ -6,13 +6,14 @@ namespace AgenTerra.Core.Reasoning;
/// Provides step-by-step reasoning capabilities with in-memory session storage.
/// This implementation is thread-safe and supports multiple concurrent sessions.
///
-public class ReasoningTool : IReasoningTool
+public class ReasoningTool : IReasoningTool, IDisposable
{
private readonly Dictionary> _sessions = new();
- private readonly object _lock = new();
+ private readonly SemaphoreSlim _lock = new(1, 1);
+ private bool _disposed;
///
- public Task ThinkAsync(ThinkInput input)
+ public async Task ThinkAsync(ThinkInput input, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(input);
@@ -38,15 +39,17 @@ public Task ThinkAsync(ThinkInput input)
content.AppendLine($"Action: {input.Action}");
}
- var step = new ReasoningStep(
- Type: "think",
- Title: input.Title,
- Content: content.ToString().TrimEnd(),
- Confidence: input.Confidence,
- Timestamp: DateTime.UtcNow
- );
-
- lock (_lock)
+ var step = new ReasoningStep
+ {
+ Type = "think",
+ Title = input.Title,
+ Content = content.ToString().TrimEnd(),
+ Confidence = input.Confidence,
+ Timestamp = DateTime.UtcNow
+ };
+
+ await _lock.WaitAsync(cancellationToken);
+ try
{
if (!_sessions.TryGetValue(input.SessionId, out var steps))
{
@@ -55,6 +58,10 @@ public Task ThinkAsync(ThinkInput input)
}
steps.Add(step);
}
+ finally
+ {
+ _lock.Release();
+ }
var response = new StringBuilder();
response.AppendLine($"[THINK] {input.Title}");
@@ -62,11 +69,11 @@ public Task ThinkAsync(ThinkInput input)
response.AppendLine(content.ToString());
response.AppendLine($"Recorded at: {step.Timestamp:O}");
- return Task.FromResult(response.ToString());
+ return response.ToString();
}
///
- public Task AnalyzeAsync(AnalyzeInput input)
+ public async Task AnalyzeAsync(AnalyzeInput input, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(input);
@@ -95,15 +102,17 @@ public Task AnalyzeAsync(AnalyzeInput input)
content.AppendLine($"Analysis: {input.Analysis}");
content.AppendLine($"Next Action: {input.NextAction}");
- var step = new ReasoningStep(
- Type: "analyze",
- Title: input.Title,
- Content: content.ToString().TrimEnd(),
- Confidence: input.Confidence,
- Timestamp: DateTime.UtcNow
- );
-
- lock (_lock)
+ var step = new ReasoningStep
+ {
+ Type = "analyze",
+ Title = input.Title,
+ Content = content.ToString().TrimEnd(),
+ Confidence = input.Confidence,
+ Timestamp = DateTime.UtcNow
+ };
+
+ await _lock.WaitAsync(cancellationToken);
+ try
{
if (!_sessions.TryGetValue(input.SessionId, out var steps))
{
@@ -112,6 +121,10 @@ public Task AnalyzeAsync(AnalyzeInput input)
}
steps.Add(step);
}
+ finally
+ {
+ _lock.Release();
+ }
var response = new StringBuilder();
response.AppendLine($"[ANALYZE] {input.Title}");
@@ -119,7 +132,7 @@ public Task AnalyzeAsync(AnalyzeInput input)
response.AppendLine(content.ToString());
response.AppendLine($"Recorded at: {step.Timestamp:O}");
- return Task.FromResult(response.ToString());
+ return response.ToString();
}
///
@@ -130,7 +143,10 @@ public IReadOnlyList GetReasoningHistory(string sessionId)
throw new ArgumentException("SessionId cannot be null or whitespace.", nameof(sessionId));
}
- lock (_lock)
+ // Using Wait() explicitly as this is a synchronous method.
+ // The method is synchronous to maintain backward compatibility and avoid forcing async all the way up the call chain.
+ _lock.Wait(CancellationToken.None);
+ try
{
if (_sessions.TryGetValue(sessionId, out var steps))
{
@@ -138,5 +154,22 @@ public IReadOnlyList GetReasoningHistory(string sessionId)
}
return Array.Empty();
}
+ finally
+ {
+ _lock.Release();
+ }
+ }
+
+ ///
+ /// Disposes the resources used by the ReasoningTool.
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _lock.Dispose();
+ _disposed = true;
+ }
+ GC.SuppressFinalize(this);
}
}
diff --git a/src/AgenTerra.Core/ServiceCollectionExtensions.cs b/src/AgenTerra.Core/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..0543ab4
--- /dev/null
+++ b/src/AgenTerra.Core/ServiceCollectionExtensions.cs
@@ -0,0 +1,37 @@
+using AgenTerra.Core.Knowledge;
+using AgenTerra.Core.Reasoning;
+using AgenTerra.Core.State;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AgenTerra.Core;
+
+///
+/// Extension methods for configuring AgenTerra services in an .
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Adds AgenTerra core services to the specified .
+ ///
+ /// The to add services to.
+ /// The so that additional calls can be chained.
+ ///
+ /// Note: ReasoningTool and InMemoryWorkflowStateStore implement IDisposable and are registered as singletons.
+ /// They will be automatically disposed when the DI container is disposed (typically on application shutdown).
+ /// Ensure the application's service provider is properly disposed to release these resources.
+ ///
+ public static IServiceCollection AddAgenTerra(this IServiceCollection services)
+ {
+ // Reasoning - Singleton for shared session management
+ services.AddSingleton();
+
+ // State - Singleton for shared state storage
+ services.AddSingleton();
+
+ // Knowledge - Singleton for shared factory with registered readers
+ // The factory creates and manages its own document reader instances
+ services.AddSingleton();
+
+ return services;
+ }
+}
diff --git a/src/AgenTerra.Core/State/IWorkflowStateStore.cs b/src/AgenTerra.Core/State/IWorkflowStateStore.cs
index 22614aa..5d3c0d0 100644
--- a/src/AgenTerra.Core/State/IWorkflowStateStore.cs
+++ b/src/AgenTerra.Core/State/IWorkflowStateStore.cs
@@ -12,18 +12,20 @@ public interface IWorkflowStateStore
/// Retrieves a workflow session by its unique identifier.
///
/// The unique identifier of the session to retrieve.
+ /// Cancellation token.
///
/// A task that represents the asynchronous operation.
/// The task result contains the workflow session if found; otherwise, null.
///
- Task GetSessionAsync(string sessionId);
+ Task GetSessionAsync(string sessionId, CancellationToken cancellationToken = default);
///
/// Saves or updates a workflow session in the store.
///
/// The workflow session to save or update.
+ /// Cancellation token.
/// A task that represents the asynchronous save operation.
- Task SaveSessionAsync(WorkflowSession session);
+ Task SaveSessionAsync(WorkflowSession session, CancellationToken cancellationToken = default);
///
/// Retrieves a specific state value from a session.
@@ -31,11 +33,12 @@ public interface IWorkflowStateStore
/// The type of the state value to retrieve.
/// The unique identifier of the session.
/// The key identifying the state value.
+ /// Cancellation token.
///
/// A task that represents the asynchronous operation.
/// The task result contains the state value if found; otherwise, the default value for type T.
///
- Task GetStateAsync(string sessionId, string key);
+ Task GetStateAsync(string sessionId, string key, CancellationToken cancellationToken = default);
///
/// Sets a specific state value in a session, creating the session if it doesn't exist.
@@ -44,25 +47,28 @@ public interface IWorkflowStateStore
/// The unique identifier of the session.
/// The key identifying the state value.
/// The value to store.
+ /// Cancellation token.
/// A task that represents the asynchronous set operation.
- Task SetStateAsync(string sessionId, string key, T value);
+ Task SetStateAsync(string sessionId, string key, T value, CancellationToken cancellationToken = default);
///
/// Deletes a workflow session from the store.
///
/// The unique identifier of the session to delete.
+ /// Cancellation token.
///
/// A task that represents the asynchronous operation.
/// The task result is true if the session was deleted; false if it didn't exist.
///
- Task DeleteSessionAsync(string sessionId);
+ Task DeleteSessionAsync(string sessionId, CancellationToken cancellationToken = default);
///
/// Retrieves all active session identifiers.
///
+ /// Cancellation token.
///
/// A task that represents the asynchronous operation.
/// The task result contains a read-only list of all session identifiers.
///
- Task> GetAllSessionIdsAsync();
+ Task> GetAllSessionIdsAsync(CancellationToken cancellationToken = default);
}
diff --git a/src/AgenTerra.Core/State/InMemoryWorkflowStateStore.cs b/src/AgenTerra.Core/State/InMemoryWorkflowStateStore.cs
index 581a5ba..14205b5 100644
--- a/src/AgenTerra.Core/State/InMemoryWorkflowStateStore.cs
+++ b/src/AgenTerra.Core/State/InMemoryWorkflowStateStore.cs
@@ -7,17 +7,18 @@ namespace AgenTerra.Core.State;
/// Stores workflow sessions in memory using a thread-safe dictionary.
/// Suitable for single-instance deployments and testing scenarios.
///
-public class InMemoryWorkflowStateStore : IWorkflowStateStore
+public class InMemoryWorkflowStateStore : IWorkflowStateStore, IDisposable
{
private readonly Dictionary _sessions = new();
private readonly SemaphoreSlim _lock = new(1, 1);
+ private bool _disposed;
///
- public async Task GetSessionAsync(string sessionId)
+ public async Task GetSessionAsync(string sessionId, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(sessionId);
- await _lock.WaitAsync();
+ await _lock.WaitAsync(cancellationToken);
try
{
return _sessions.TryGetValue(sessionId, out var session)
@@ -31,11 +32,11 @@ public class InMemoryWorkflowStateStore : IWorkflowStateStore
}
///
- public async Task SaveSessionAsync(WorkflowSession session)
+ public async Task SaveSessionAsync(WorkflowSession session, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
- await _lock.WaitAsync();
+ await _lock.WaitAsync(cancellationToken);
try
{
var updatedSession = session with { UpdatedAt = DateTime.UtcNow };
@@ -48,12 +49,12 @@ public async Task SaveSessionAsync(WorkflowSession session)
}
///
- public async Task GetStateAsync(string sessionId, string key)
+ public async Task GetStateAsync(string sessionId, string key, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(sessionId);
ArgumentNullException.ThrowIfNull(key);
- var session = await GetSessionAsync(sessionId);
+ var session = await GetSessionAsync(sessionId, cancellationToken);
if (session?.SessionState.TryGetValue(key, out var value) == true)
{
return (T?)value;
@@ -67,12 +68,12 @@ public async Task SaveSessionAsync(WorkflowSession session)
/// The null-forgiving operator is used because the Dictionary requires non-nullable object values,
/// but the actual value can be null when T is nullable.
///
- public async Task SetStateAsync(string sessionId, string key, T value)
+ public async Task SetStateAsync(string sessionId, string key, T value, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(sessionId);
ArgumentNullException.ThrowIfNull(key);
- await _lock.WaitAsync();
+ await _lock.WaitAsync(cancellationToken);
try
{
if (!_sessions.TryGetValue(sessionId, out var session))
@@ -104,11 +105,11 @@ public async Task SetStateAsync(string sessionId, string key, T value)
}
///
- public async Task DeleteSessionAsync(string sessionId)
+ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(sessionId);
- await _lock.WaitAsync();
+ await _lock.WaitAsync(cancellationToken);
try
{
return _sessions.Remove(sessionId);
@@ -120,9 +121,9 @@ public async Task DeleteSessionAsync(string sessionId)
}
///
- public async Task> GetAllSessionIdsAsync()
+ public async Task> GetAllSessionIdsAsync(CancellationToken cancellationToken = default)
{
- await _lock.WaitAsync();
+ await _lock.WaitAsync(cancellationToken);
try
{
return _sessions.Keys.ToList();
@@ -132,4 +133,18 @@ public async Task> GetAllSessionIdsAsync()
_lock.Release();
}
}
+
+ ///
+ /// Disposes the resources used by the InMemoryWorkflowStateStore.
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _lock.Dispose();
+ _disposed = true;
+ }
+ GC.SuppressFinalize(this);
+ GC.SuppressFinalize(this);
+ }
}
diff --git a/src/AgenTerra.Core/State/Models/WorkflowStateException.cs b/src/AgenTerra.Core/State/Models/WorkflowStateException.cs
new file mode 100644
index 0000000..e65377c
--- /dev/null
+++ b/src/AgenTerra.Core/State/Models/WorkflowStateException.cs
@@ -0,0 +1,21 @@
+namespace AgenTerra.Core.State.Models;
+
+///
+/// Exception thrown when workflow state operations fail.
+///
+public class WorkflowStateException : Exception
+{
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The message that describes the error.
+ public WorkflowStateException(string message) : base(message) { }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message and a reference to the inner exception.
+ ///
+ /// The message that describes the error.
+ /// The exception that is the cause of the current exception.
+ public WorkflowStateException(string message, Exception innerException)
+ : base(message, innerException) { }
+}
diff --git a/tests/AgenTerra.Core.Tests/Reasoning/ReasoningHistoryTests.cs b/tests/AgenTerra.Core.Tests/Reasoning/ReasoningHistoryTests.cs
index ad74854..98b74f7 100644
--- a/tests/AgenTerra.Core.Tests/Reasoning/ReasoningHistoryTests.cs
+++ b/tests/AgenTerra.Core.Tests/Reasoning/ReasoningHistoryTests.cs
@@ -8,7 +8,7 @@ public class ReasoningHistoryTests
public async Task GetReasoningHistory_ReturnsReadOnlyList()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "readonly-test";
await tool.ThinkAsync(new ThinkInput(sessionId, "Test", "Thought"));
@@ -24,7 +24,7 @@ public async Task GetReasoningHistory_ReturnsReadOnlyList()
public async Task ReasoningStep_ContainsCorrectType()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "type-test";
// Act
@@ -42,7 +42,7 @@ public async Task ReasoningStep_ContainsCorrectType()
public async Task ReasoningStep_ContainsCorrectTitle()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "title-test";
// Act
@@ -58,15 +58,12 @@ public async Task ReasoningStep_ContainsCorrectTitle()
public async Task ThinkStep_ContentIncludesThoughtAndAction()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "content-test";
// Act
- await tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: "Test",
- Thought: "My thought",
- Action: "My action"
+ await tool.ThinkAsync(new ThinkInput(sessionId, "Test", "My thought",
+ "My action"
));
var history = tool.GetReasoningHistory(sessionId);
@@ -80,14 +77,11 @@ await tool.ThinkAsync(new ThinkInput(
public async Task ThinkStep_ContentWithoutAction_OnlyIncludesThought()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "no-action-test";
// Act
- await tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: "Test",
- Thought: "My thought"
+ await tool.ThinkAsync(new ThinkInput(sessionId, "Test", "My thought"
));
var history = tool.GetReasoningHistory(sessionId);
@@ -101,16 +95,12 @@ await tool.ThinkAsync(new ThinkInput(
public async Task AnalyzeStep_ContentIncludesResultAnalysisAndNextAction()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "analyze-content-test";
// Act
- await tool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: "Test",
- Result: "My result",
- Analysis: "My analysis",
- NextAction: NextAction.Validate
+ await tool.AnalyzeAsync(new AnalyzeInput(sessionId, "Test", "My result", "My analysis",
+ NextAction.Validate
));
var history = tool.GetReasoningHistory(sessionId);
@@ -125,12 +115,12 @@ await tool.AnalyzeAsync(new AnalyzeInput(
public async Task ReasoningStep_ConfidenceIsStoredCorrectly()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "confidence-test";
// Act
- await tool.ThinkAsync(new ThinkInput(sessionId, "Test", "Thought", Confidence: 0.65));
- await tool.AnalyzeAsync(new AnalyzeInput(sessionId, "Test", "R", "A", Confidence: 0.92));
+ await tool.ThinkAsync(new ThinkInput(sessionId, "Test", "Thought", null, 0.65));
+ await tool.AnalyzeAsync(new AnalyzeInput(sessionId, "Test", "R", "A", NextAction.Continue, 0.92));
var history = tool.GetReasoningHistory(sessionId);
@@ -143,7 +133,7 @@ public async Task ReasoningStep_ConfidenceIsStoredCorrectly()
public async Task ReasoningStep_TimestampsAreChronological()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "timestamp-test";
// Act
@@ -165,16 +155,11 @@ public async Task ReasoningStep_TimestampsAreChronological()
public async Task ReasoningStep_AllFieldsArePopulated()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "complete-test";
// Act
- await tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: "Complete Step",
- Thought: "Full thought",
- Action: "Full action",
- Confidence: 0.88
+ await tool.ThinkAsync(new ThinkInput(sessionId, "Complete Step", "Full thought", "Full action", 0.88
));
var history = tool.GetReasoningHistory(sessionId);
@@ -195,7 +180,7 @@ await tool.ThinkAsync(new ThinkInput(
public async Task GetReasoningHistory_AfterMultipleCalls_ReturnsConsistentData()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "consistency-test";
await tool.ThinkAsync(new ThinkInput(sessionId, "Test", "Thought"));
diff --git a/tests/AgenTerra.Core.Tests/Reasoning/ReasoningToolTests.cs b/tests/AgenTerra.Core.Tests/Reasoning/ReasoningToolTests.cs
index cd737eb..a8b72bf 100644
--- a/tests/AgenTerra.Core.Tests/Reasoning/ReasoningToolTests.cs
+++ b/tests/AgenTerra.Core.Tests/Reasoning/ReasoningToolTests.cs
@@ -8,13 +8,8 @@ public class ReasoningToolTests
public async Task ThinkAsync_WithValidInput_ReturnsFormattedResponse()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new ThinkInput(
- SessionId: "test-session",
- Title: "Test Thought",
- Thought: "This is a test thought",
- Action: "Test action",
- Confidence: 0.9
+ using var tool = new ReasoningTool();
+ var input = new ThinkInput("test-session", "Test Thought", "This is a test thought", "Test action", 0.9
);
// Act
@@ -32,7 +27,7 @@ public async Task ThinkAsync_WithValidInput_ReturnsFormattedResponse()
public async Task ThinkAsync_WithNullInput_ThrowsArgumentNullException()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
// Act & Assert
await Assert.ThrowsAsync(() => tool.ThinkAsync(null!));
@@ -42,11 +37,8 @@ public async Task ThinkAsync_WithNullInput_ThrowsArgumentNullException()
public async Task ThinkAsync_WithNullSessionId_ThrowsArgumentException()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new ThinkInput(
- SessionId: null!,
- Title: "Test",
- Thought: "Test"
+ using var tool = new ReasoningTool();
+ var input = new ThinkInput(null!, "Test", "Test"
);
// Act & Assert
@@ -57,11 +49,8 @@ public async Task ThinkAsync_WithNullSessionId_ThrowsArgumentException()
public async Task ThinkAsync_WithEmptyTitle_ThrowsArgumentException()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new ThinkInput(
- SessionId: "test-session",
- Title: "",
- Thought: "Test"
+ using var tool = new ReasoningTool();
+ var input = new ThinkInput("test-session", "", "Test"
);
// Act & Assert
@@ -72,11 +61,8 @@ public async Task ThinkAsync_WithEmptyTitle_ThrowsArgumentException()
public async Task ThinkAsync_WithEmptyThought_ThrowsArgumentException()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new ThinkInput(
- SessionId: "test-session",
- Title: "Test",
- Thought: ""
+ using var tool = new ReasoningTool();
+ var input = new ThinkInput("test-session", "Test", ""
);
// Act & Assert
@@ -87,11 +73,8 @@ public async Task ThinkAsync_WithEmptyThought_ThrowsArgumentException()
public async Task ThinkAsync_WithoutAction_ReturnsResponseWithoutAction()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new ThinkInput(
- SessionId: "test-session",
- Title: "Test Thought",
- Thought: "This is a test thought"
+ using var tool = new ReasoningTool();
+ var input = new ThinkInput("test-session", "Test Thought", "This is a test thought"
);
// Act
@@ -107,14 +90,8 @@ public async Task ThinkAsync_WithoutAction_ReturnsResponseWithoutAction()
public async Task AnalyzeAsync_WithValidInput_ReturnsFormattedResponse()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new AnalyzeInput(
- SessionId: "test-session",
- Title: "Test Analysis",
- Result: "Test result",
- Analysis: "Test analysis",
- NextAction: NextAction.Validate,
- Confidence: 0.85
+ using var tool = new ReasoningTool();
+ var input = new AnalyzeInput("test-session", "Test Analysis", "Test result", "Test analysis", NextAction.Validate, 0.85
);
// Act
@@ -133,7 +110,7 @@ public async Task AnalyzeAsync_WithValidInput_ReturnsFormattedResponse()
public async Task AnalyzeAsync_WithNullInput_ThrowsArgumentNullException()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
// Act & Assert
await Assert.ThrowsAsync(() => tool.AnalyzeAsync(null!));
@@ -143,12 +120,8 @@ public async Task AnalyzeAsync_WithNullInput_ThrowsArgumentNullException()
public async Task AnalyzeAsync_WithNullSessionId_ThrowsArgumentException()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new AnalyzeInput(
- SessionId: null!,
- Title: "Test",
- Result: "Test",
- Analysis: "Test"
+ using var tool = new ReasoningTool();
+ var input = new AnalyzeInput(null!, "Test", "Test", "Test"
);
// Act & Assert
@@ -159,12 +132,8 @@ public async Task AnalyzeAsync_WithNullSessionId_ThrowsArgumentException()
public async Task AnalyzeAsync_WithEmptyResult_ThrowsArgumentException()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new AnalyzeInput(
- SessionId: "test-session",
- Title: "Test",
- Result: "",
- Analysis: "Test"
+ using var tool = new ReasoningTool();
+ var input = new AnalyzeInput("test-session", "Test", "", "Test"
);
// Act & Assert
@@ -175,12 +144,8 @@ public async Task AnalyzeAsync_WithEmptyResult_ThrowsArgumentException()
public async Task AnalyzeAsync_WithDefaultNextAction_UsesContinue()
{
// Arrange
- var tool = new ReasoningTool();
- var input = new AnalyzeInput(
- SessionId: "test-session",
- Title: "Test Analysis",
- Result: "Test result",
- Analysis: "Test analysis"
+ using var tool = new ReasoningTool();
+ var input = new AnalyzeInput("test-session", "Test Analysis", "Test result", "Test analysis"
);
// Act
@@ -194,7 +159,7 @@ public async Task AnalyzeAsync_WithDefaultNextAction_UsesContinue()
public void GetReasoningHistory_WithNullSessionId_ThrowsArgumentException()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
// Act & Assert
Assert.Throws(() => tool.GetReasoningHistory(null!));
@@ -204,7 +169,7 @@ public void GetReasoningHistory_WithNullSessionId_ThrowsArgumentException()
public void GetReasoningHistory_WithNonExistentSession_ReturnsEmptyList()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
// Act
var history = tool.GetReasoningHistory("non-existent-session");
@@ -218,7 +183,7 @@ public void GetReasoningHistory_WithNonExistentSession_ReturnsEmptyList()
public async Task GetReasoningHistory_AfterAddingSteps_ReturnsAllStepsInOrder()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "test-session";
await tool.ThinkAsync(new ThinkInput(sessionId, "First", "First thought"));
@@ -242,7 +207,7 @@ public async Task GetReasoningHistory_AfterAddingSteps_ReturnsAllStepsInOrder()
public async Task GetReasoningHistory_ReturnsSnapshotOfCurrentState()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "test-session";
await tool.ThinkAsync(new ThinkInput(sessionId, "First", "First thought"));
@@ -266,7 +231,7 @@ public async Task GetReasoningHistory_ReturnsSnapshotOfCurrentState()
public async Task ReasoningSteps_ContainCorrectTimestamps()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "test-session";
var beforeTime = DateTime.UtcNow;
@@ -286,12 +251,12 @@ public async Task ReasoningSteps_ContainCorrectTimestamps()
public async Task ReasoningSteps_StoreCorrectConfidence()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "test-session";
// Act
- await tool.ThinkAsync(new ThinkInput(sessionId, "Test", "Test", Confidence: 0.75));
- await tool.AnalyzeAsync(new AnalyzeInput(sessionId, "Test", "R", "A", Confidence: 0.95));
+ await tool.ThinkAsync(new ThinkInput(sessionId, "Test", "Test", null, 0.75));
+ await tool.AnalyzeAsync(new AnalyzeInput(sessionId, "Test", "R", "A", NextAction.Continue, 0.95));
var history = tool.GetReasoningHistory(sessionId);
@@ -299,4 +264,34 @@ public async Task ReasoningSteps_StoreCorrectConfidence()
Assert.Equal(0.75, history[0].Confidence);
Assert.Equal(0.95, history[1].Confidence);
}
+
+ [Fact]
+ public async Task ThinkAsync_WithCancellationToken_SupportsCancellation()
+ {
+ // Arrange
+ using var tool = new ReasoningTool();
+ var input = new ThinkInput("test-session", "Test Thought", "This is a test thought"
+ );
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await tool.ThinkAsync(input, cts.Token));
+ }
+
+ [Fact]
+ public async Task AnalyzeAsync_WithCancellationToken_SupportsCancellation()
+ {
+ // Arrange
+ using var tool = new ReasoningTool();
+ var input = new AnalyzeInput("test-session", "Test Analysis", "Test result", "Test analysis"
+ );
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await tool.AnalyzeAsync(input, cts.Token));
+ }
}
diff --git a/tests/AgenTerra.Core.Tests/Reasoning/SessionManagementTests.cs b/tests/AgenTerra.Core.Tests/Reasoning/SessionManagementTests.cs
index 148bcd8..6f4b9d9 100644
--- a/tests/AgenTerra.Core.Tests/Reasoning/SessionManagementTests.cs
+++ b/tests/AgenTerra.Core.Tests/Reasoning/SessionManagementTests.cs
@@ -8,7 +8,7 @@ public class SessionManagementTests
public async Task DifferentSessions_AreIsolatedFromEachOther()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId1 = "session-1";
var sessionId2 = "session-2";
@@ -31,7 +31,7 @@ public async Task DifferentSessions_AreIsolatedFromEachOther()
public async Task MultipleSessions_CanBeCreatedConcurrently()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionCount = 10;
var tasks = new List();
@@ -39,10 +39,7 @@ public async Task MultipleSessions_CanBeCreatedConcurrently()
for (int i = 0; i < sessionCount; i++)
{
var sessionId = $"session-{i}";
- var task = tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: $"Session {i}",
- Thought: $"Thought for session {i}"
+ var task = tool.ThinkAsync(new ThinkInput(sessionId, $"Session {i}", $"Thought for session {i}"
));
tasks.Add(task);
}
@@ -62,7 +59,7 @@ public async Task MultipleSessions_CanBeCreatedConcurrently()
public async Task SameSession_AccumulatesStepsOverTime()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "accumulation-test";
// Act & Assert
@@ -83,7 +80,7 @@ public async Task SameSession_AccumulatesStepsOverTime()
public async Task NewSession_IsCreatedAutomaticallyOnFirstUse()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "auto-created-session";
// Act
@@ -100,7 +97,7 @@ public async Task NewSession_IsCreatedAutomaticallyOnFirstUse()
public async Task SessionHistory_PreservesInsertionOrder()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "order-test";
var expectedTitles = new List();
@@ -134,7 +131,7 @@ public async Task SessionHistory_PreservesInsertionOrder()
public async Task MixedStepTypes_AreCorrectlyStoredInSession()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "mixed-types-test";
// Act
diff --git a/tests/AgenTerra.Core.Tests/Reasoning/ThreadSafetyTests.cs b/tests/AgenTerra.Core.Tests/Reasoning/ThreadSafetyTests.cs
index 8b69bce..252f405 100644
--- a/tests/AgenTerra.Core.Tests/Reasoning/ThreadSafetyTests.cs
+++ b/tests/AgenTerra.Core.Tests/Reasoning/ThreadSafetyTests.cs
@@ -8,7 +8,7 @@ public class ThreadSafetyTests
public async Task ConcurrentThinkCalls_ToSameSession_AreThreadSafe()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "concurrent-think-test";
var taskCount = 100;
var tasks = new List();
@@ -17,10 +17,7 @@ public async Task ConcurrentThinkCalls_ToSameSession_AreThreadSafe()
for (int i = 0; i < taskCount; i++)
{
var index = i;
- var task = tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: $"Concurrent Think {index}",
- Thought: $"Thought {index}"
+ var task = tool.ThinkAsync(new ThinkInput(sessionId, $"Concurrent Think {index}", $"Thought {index}"
));
tasks.Add(task);
}
@@ -38,7 +35,7 @@ public async Task ConcurrentThinkCalls_ToSameSession_AreThreadSafe()
public async Task ConcurrentAnalyzeCalls_ToSameSession_AreThreadSafe()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "concurrent-analyze-test";
var taskCount = 100;
var tasks = new List();
@@ -47,11 +44,7 @@ public async Task ConcurrentAnalyzeCalls_ToSameSession_AreThreadSafe()
for (int i = 0; i < taskCount; i++)
{
var index = i;
- var task = tool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: $"Concurrent Analyze {index}",
- Result: $"Result {index}",
- Analysis: $"Analysis {index}"
+ var task = tool.AnalyzeAsync(new AnalyzeInput(sessionId, $"Concurrent Analyze {index}", $"Result {index}", $"Analysis {index}"
));
tasks.Add(task);
}
@@ -69,7 +62,7 @@ public async Task ConcurrentAnalyzeCalls_ToSameSession_AreThreadSafe()
public async Task ConcurrentMixedCalls_ToSameSession_AreThreadSafe()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "concurrent-mixed-test";
var taskCount = 100;
var tasks = new List();
@@ -82,19 +75,12 @@ public async Task ConcurrentMixedCalls_ToSameSession_AreThreadSafe()
if (i % 2 == 0)
{
- task = tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: $"Think {index}",
- Thought: $"Thought {index}"
+ task = tool.ThinkAsync(new ThinkInput(sessionId, $"Think {index}", $"Thought {index}"
));
}
else
{
- task = tool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: $"Analyze {index}",
- Result: $"Result {index}",
- Analysis: $"Analysis {index}"
+ task = tool.AnalyzeAsync(new AnalyzeInput(sessionId, $"Analyze {index}", $"Result {index}", $"Analysis {index}"
));
}
@@ -113,7 +99,7 @@ public async Task ConcurrentMixedCalls_ToSameSession_AreThreadSafe()
public async Task ConcurrentCallsToMultipleSessions_AreThreadSafe()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionCount = 50;
var stepsPerSession = 20;
var tasks = new List();
@@ -126,10 +112,7 @@ public async Task ConcurrentCallsToMultipleSessions_AreThreadSafe()
for (int i = 0; i < stepsPerSession; i++)
{
var index = i;
- var task = tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: $"Step {index}",
- Thought: $"Thought {index}"
+ var task = tool.ThinkAsync(new ThinkInput(sessionId, $"Step {index}", $"Thought {index}"
));
tasks.Add(task);
}
@@ -149,7 +132,7 @@ public async Task ConcurrentCallsToMultipleSessions_AreThreadSafe()
public async Task ConcurrentReads_WhileWriting_AreThreadSafe()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionId = "read-write-test";
var writeCount = 100;
var readCount = 100;
@@ -159,10 +142,7 @@ public async Task ConcurrentReads_WhileWriting_AreThreadSafe()
for (int i = 0; i < writeCount; i++)
{
var index = i;
- var task = tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: $"Step {index}",
- Thought: $"Thought {index}"
+ var task = tool.ThinkAsync(new ThinkInput(sessionId, $"Step {index}", $"Thought {index}"
));
tasks.Add(task);
}
@@ -186,15 +166,12 @@ public async Task ConcurrentReads_WhileWriting_AreThreadSafe()
public async Task ParallelSessionCreation_IsThreadSafe()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionCount = 100;
// Act
var tasks = Enumerable.Range(0, sessionCount)
- .Select(i => tool.ThinkAsync(new ThinkInput(
- SessionId: $"session-{i}",
- Title: "Initial Step",
- Thought: "Initial thought"
+ .Select(i => tool.ThinkAsync(new ThinkInput($"session-{i}", "Initial Step", "Initial thought"
)))
.ToList();
@@ -212,7 +189,7 @@ public async Task ParallelSessionCreation_IsThreadSafe()
public async Task StressTest_ManyOperationsSimultaneously()
{
// Arrange
- var tool = new ReasoningTool();
+ using var tool = new ReasoningTool();
var sessionCount = 10;
var operationsPerSession = 50;
var random = new Random(42); // Fixed seed for reproducibility
@@ -233,19 +210,12 @@ public async Task StressTest_ManyOperationsSimultaneously()
if (operation == 0)
{
- task = tool.ThinkAsync(new ThinkInput(
- SessionId: sessionId,
- Title: $"Think {index}",
- Thought: $"Thought {index}"
+ task = tool.ThinkAsync(new ThinkInput(sessionId, $"Think {index}", $"Thought {index}"
));
}
else if (operation == 1)
{
- task = tool.AnalyzeAsync(new AnalyzeInput(
- SessionId: sessionId,
- Title: $"Analyze {index}",
- Result: $"Result {index}",
- Analysis: $"Analysis {index}"
+ task = tool.AnalyzeAsync(new AnalyzeInput(sessionId, $"Analyze {index}", $"Result {index}", $"Analysis {index}"
));
}
else
diff --git a/tests/AgenTerra.Core.Tests/State/InMemoryWorkflowStateStoreTests.cs b/tests/AgenTerra.Core.Tests/State/InMemoryWorkflowStateStoreTests.cs
index 6f72330..7370d5e 100644
--- a/tests/AgenTerra.Core.Tests/State/InMemoryWorkflowStateStoreTests.cs
+++ b/tests/AgenTerra.Core.Tests/State/InMemoryWorkflowStateStoreTests.cs
@@ -9,7 +9,7 @@ public class InMemoryWorkflowStateStoreTests
public async Task GetSessionAsync_ReturnsNull_ForNonExistentSession()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "non-existent-session";
// Act
@@ -23,7 +23,7 @@ public async Task GetSessionAsync_ReturnsNull_ForNonExistentSession()
public async Task GetSessionAsync_ThrowsArgumentNullException_WhenSessionIdIsNull()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act & Assert
await Assert.ThrowsAsync(() => store.GetSessionAsync(null!));
@@ -33,7 +33,7 @@ public async Task GetSessionAsync_ThrowsArgumentNullException_WhenSessionIdIsNul
public async Task SaveSessionAsync_CreatesNewSession()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var session = new WorkflowSession
{
SessionId = "test-session",
@@ -53,7 +53,7 @@ public async Task SaveSessionAsync_CreatesNewSession()
public async Task SaveSessionAsync_ThrowsArgumentNullException_WhenSessionIsNull()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act & Assert
await Assert.ThrowsAsync(() => store.SaveSessionAsync(null!));
@@ -63,7 +63,7 @@ public async Task SaveSessionAsync_ThrowsArgumentNullException_WhenSessionIsNull
public async Task SaveSessionAsync_UpdatesExistingSessionTimestamp()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var session = new WorkflowSession
{
SessionId = "test-session",
@@ -88,7 +88,7 @@ public async Task SaveSessionAsync_UpdatesExistingSessionTimestamp()
public async Task GetStateAsync_ReturnsCorrectTypedValue()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "test-session";
var expectedValue = "test-value";
@@ -104,7 +104,7 @@ public async Task GetStateAsync_ReturnsCorrectTypedValue()
public async Task GetStateAsync_ReturnsDefault_ForMissingKey()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "test-session";
// Act
@@ -119,7 +119,7 @@ public async Task GetStateAsync_ReturnsDefault_ForMissingKey()
public async Task GetStateAsync_ReturnsDefault_ForNonExistentSession()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act
var result = await store.GetStateAsync("non-existent", "key1");
@@ -132,7 +132,7 @@ public async Task GetStateAsync_ReturnsDefault_ForNonExistentSession()
public async Task GetStateAsync_ThrowsArgumentNullException_WhenSessionIdIsNull()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act & Assert
await Assert.ThrowsAsync(() => store.GetStateAsync(null!, "key"));
@@ -142,7 +142,7 @@ public async Task GetStateAsync_ThrowsArgumentNullException_WhenSessionIdIsNull(
public async Task GetStateAsync_ThrowsArgumentNullException_WhenKeyIsNull()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act & Assert
await Assert.ThrowsAsync(() => store.GetStateAsync("session", null!));
@@ -152,7 +152,7 @@ public async Task GetStateAsync_ThrowsArgumentNullException_WhenKeyIsNull()
public async Task SetStateAsync_CreatesSession_IfNotExists()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "new-session";
// Act
@@ -168,7 +168,7 @@ public async Task SetStateAsync_CreatesSession_IfNotExists()
public async Task SetStateAsync_ThrowsArgumentNullException_WhenSessionIdIsNull()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act & Assert
await Assert.ThrowsAsync(() => store.SetStateAsync(null!, "key", "value"));
@@ -178,7 +178,7 @@ public async Task SetStateAsync_ThrowsArgumentNullException_WhenSessionIdIsNull(
public async Task SetStateAsync_ThrowsArgumentNullException_WhenKeyIsNull()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act & Assert
await Assert.ThrowsAsync(() => store.SetStateAsync("session", null!, "value"));
@@ -188,7 +188,7 @@ public async Task SetStateAsync_ThrowsArgumentNullException_WhenKeyIsNull()
public async Task SetStateAsync_UpdatesStateCorrectly()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "test-session";
// Act
@@ -206,7 +206,7 @@ public async Task SetStateAsync_UpdatesStateCorrectly()
public async Task SetStateAsync_PreservesImmutability()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "test-session";
// Act
@@ -226,7 +226,7 @@ public async Task SetStateAsync_PreservesImmutability()
public async Task DeleteSessionAsync_RemovesSession()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "test-session";
await store.SetStateAsync(sessionId, "key1", "value1");
@@ -243,7 +243,7 @@ public async Task DeleteSessionAsync_RemovesSession()
public async Task DeleteSessionAsync_ReturnsFalse_ForNonExistentSession()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act
var result = await store.DeleteSessionAsync("non-existent");
@@ -256,7 +256,7 @@ public async Task DeleteSessionAsync_ReturnsFalse_ForNonExistentSession()
public async Task DeleteSessionAsync_ThrowsArgumentNullException_WhenSessionIdIsNull()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act & Assert
await Assert.ThrowsAsync(() => store.DeleteSessionAsync(null!));
@@ -266,7 +266,7 @@ public async Task DeleteSessionAsync_ThrowsArgumentNullException_WhenSessionIdIs
public async Task GetAllSessionIdsAsync_ReturnsAllSessions()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
await store.SetStateAsync("session1", "key", "value");
await store.SetStateAsync("session2", "key", "value");
await store.SetStateAsync("session3", "key", "value");
@@ -285,7 +285,7 @@ public async Task GetAllSessionIdsAsync_ReturnsAllSessions()
public async Task GetAllSessionIdsAsync_ReturnsEmpty_WhenNoSessions()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
// Act
var sessionIds = await store.GetAllSessionIdsAsync();
@@ -298,7 +298,7 @@ public async Task GetAllSessionIdsAsync_ReturnsEmpty_WhenNoSessions()
public async Task ConcurrentOperations_AreThreadSafe()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "concurrent-session";
var tasks = new List();
@@ -324,7 +324,7 @@ public async Task ConcurrentOperations_AreThreadSafe()
public async Task SetStateAsync_SupportsComplexTypes()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "test-session";
var complexObject = new List { "item1", "item2", "item3" };
@@ -342,7 +342,7 @@ public async Task SetStateAsync_SupportsComplexTypes()
public async Task GetSessionAsync_ReturnsCopy_NotReference()
{
// Arrange
- var store = new InMemoryWorkflowStateStore();
+ using var store = new InMemoryWorkflowStateStore();
var sessionId = "test-session";
await store.SetStateAsync(sessionId, "key1", "value1");
@@ -354,4 +354,87 @@ public async Task GetSessionAsync_ReturnsCopy_NotReference()
Assert.NotSame(session1, session2);
Assert.Equal(session1!.SessionId, session2!.SessionId);
}
+
+ [Fact]
+ public async Task GetSessionAsync_WithCancellationToken_SupportsCancellation()
+ {
+ // Arrange
+ using var store = new InMemoryWorkflowStateStore();
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await store.GetSessionAsync("test-session", cts.Token));
+ }
+
+ [Fact]
+ public async Task SaveSessionAsync_WithCancellationToken_SupportsCancellation()
+ {
+ // Arrange
+ using var store = new InMemoryWorkflowStateStore();
+ var session = new WorkflowSession
+ {
+ SessionId = "test-session",
+ SessionState = new Dictionary()
+ };
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await store.SaveSessionAsync(session, cts.Token));
+ }
+
+ [Fact]
+ public async Task GetStateAsync_WithCancellationToken_SupportsCancellation()
+ {
+ // Arrange
+ using var store = new InMemoryWorkflowStateStore();
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await store.GetStateAsync("test-session", "key", cts.Token));
+ }
+
+ [Fact]
+ public async Task SetStateAsync_WithCancellationToken_SupportsCancellation()
+ {
+ // Arrange
+ using var store = new InMemoryWorkflowStateStore();
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await store.SetStateAsync("test-session", "key", "value", cts.Token));
+ }
+
+ [Fact]
+ public async Task DeleteSessionAsync_WithCancellationToken_SupportsCancellation()
+ {
+ // Arrange
+ using var store = new InMemoryWorkflowStateStore();
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await store.DeleteSessionAsync("test-session", cts.Token));
+ }
+
+ [Fact]
+ public async Task GetAllSessionIdsAsync_WithCancellationToken_SupportsCancellation()
+ {
+ // Arrange
+ using var store = new InMemoryWorkflowStateStore();
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await store.GetAllSessionIdsAsync(cts.Token));
+ }
}