Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,135 @@ All notable changes to **Delibera** are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [10.2.4] - 2026

This release delivers **full cooperative `CancellationToken` support across every
public async method** — a single cancel signal now aborts the entire pipeline
(rounds, Chairman synthesis, LLM calls, MCP tool invocations, RAG queries and
file writes) via `OperationCanceledException`. It also adds **in-memory
`MarkdownKnowledgeBase` loaders** that ingest markdown bodies without temp
files, with optional per-source metadata.

### Added — In-memory `MarkdownKnowledgeBase` loaders

- **`KnowledgeDocument`** — new sealed record in `Delibera.Core.Knowledge`
(`string Name`, `string Content`, `IReadOnlyDictionary<string, string>? Metadata`)
used to tag documents with per-source metadata so the council can distinguish
"contract" from "discovery context" inside the KB.

- **`MarkdownKnowledgeBase.LoadTextAsync(string content, string sourceName, CancellationToken)`**
— ingest a markdown body without writing a temp file. The `sourceName`
appears in the council's per-round context, just like a file path would.

- **`MarkdownKnowledgeBase.LoadTextAsync(KnowledgeDocument, CancellationToken)`**
— overload that preserves the supplied `Metadata` on the indexed document.

- **`MarkdownKnowledgeBase.LoadTextsAsync(IEnumerable<KnowledgeDocument>, CancellationToken)`**
— sequential bulk ingest with cooperative cancellation checked between
documents.

- **`MarkdownKnowledgeBase.DocumentMetadata`** — read-only snapshot
(`IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>?>`) of the
per-source metadata captured at load time. Sources loaded through the
file-path API appear as `null` entries.

### Added — Full cooperative `CancellationToken` support

Every public async method in the library now honors a `CancellationToken`
cooperatively. A single cancel signal aborts the entire pipeline — rounds,
Chairman synthesis, LLM calls, MCP tool invocations, RAG queries and even
file writes — via `OperationCanceledException`.

#### Changed — Additive CT parameters (no breaking binary changes)

- **`IKnowledgeBase.LoadAsync(string, CancellationToken)`** and
**`IKnowledgeBase.LoadManyAsync(IEnumerable<string>, CancellationToken)`** —
`CancellationToken ct = default` added to existing method signatures.
**Source-breaking** for any external implementer of the interface;
**binary-compatible** for callers (parameter has a default value).

- **`MarkdownKnowledgeBase`** — the file-path API
(`LoadAsync`, `LoadManyAsync`, `LoadDirectoryAsync`) now accepts a
`CancellationToken` and forwards it to `File.ReadAllTextAsync` and between
files in bulk operations.

- **`DebateResult`** — all save methods
(`SaveToMarkdownAsync`, `SaveStatisticsAsync`, `SaveLogsAsync`, `SaveAllAsync`,
`SaveToFileAsync`) accept a `CancellationToken` and forward it to
`File.WriteAllTextAsync`. `SaveAllAsync` propagates the token to each
individual save call.

- **`CouncilExecutor.ExecuteAsync`** — the caller's `CancellationToken` is now
forwarded to the final `result.SaveToFileAsync(_outputPath, ct)` step,
closing the top-level cancellation chain end-to-end.

#### Added — Hosted-service helper

- **`Delibera.Core.Extensions.IAppStoppingToken`** — minimal abstraction
(`CancellationToken ApplicationStopping { get; }`) that any host lifetime
can implement in three lines without forcing a `Microsoft.Extensions.Hosting`
dependency on `Delibera.Core`.

- **`Delibera.Core.Extensions.CouncilExecutorLifetimeExtensions.ExecuteAsync(
ICouncilExecutor, IAppStoppingToken, CancellationToken)`** — links the
caller's `CancellationToken` with `IAppStoppingToken.ApplicationStopping`
via `CancellationTokenSource.CreateLinkedTokenSource` so a host shutdown
(ASP.NET Core, Worker Service, etc.) cancels the debate cooperatively.
Whichever signal fires first wins.

#### Added — ConsoleApp demo

- **`Delibera.ConsoleApp.Examples.CancellationExample`** — run with
`--cancellation`. Wires `Console.CancelKeyPress` to a
`CancellationTokenSource` and forwards the token through
`executor.ExecuteAsync(cts.Token)`. Includes a heartbeat task that proves
the main thread is alive while the debate is awaiting.

- The default `Program.Main` now also wires Ctrl+C for the main demo path
so a user can cancel any run with a single keystroke. Example-mode
dispatchers (e.g. `--cancellation`) keep ownership of their own
`CancelKeyPress` handlers and are not double-bound.

### Tests

- **9 new tests** in `MarkdownKnowledgeBaseTests` for the in-memory loaders:
string overload happy-path, `KnowledgeDocument` overload happy-path with
metadata assertion, `LoadTextsAsync` bulk happy-path, source-name
overwrite semantics, and **6 cancellation tests** covering pre-canceled
tokens on every public method plus `LoadManyAsync` / `LoadDirectoryAsync`
honoring cancellation between files.
- **7 new tests** in `DebateResultCancellationTests` covering all save
methods with pre-canceled tokens + a happy-path test for `SaveAllAsync`
that verifies the three files are produced.
- **2 new tests** in `CouncilExecutorCancellationTests` covering the
executor's CT-aware save path and a positive no-cancel run.
- **5 new tests** in `CouncilExecutorLifetimeExtensionsTests` covering the
`IAppStoppingToken` helper: caller-token respected, application-stopping
token respected, run-to-completion when neither is canceled, and
null-argument validation on both `executor` and `lifetime`.

### Documentation

- New **Cancellation Support** section in `Delibera.Core/README.md` with 4
worked examples (timeout, manual cancel, ASP.NET Core
`IHostApplicationLifetime` adapter, Console Ctrl+C) and a table of
cancellable operations.
- New **Cancellation Support** section in the repo-root `README.md` with a
quick-start example and a link to the full guide.
- Feature row added to the Key Features table in both READMEs.
- `MarkdownKnowledgeBase.LoadTextAsync` and `LoadTextsAsync` added to the
cancellable-operations table in `Delibera.Core/README.md`.

### Compatibility

- **No breaking binary changes.** All new `CancellationToken` parameters
have a default value, so existing compiled callers continue to link and
run unchanged. External implementers of `IKnowledgeBase` must add the
`CancellationToken` parameter to their method signatures — this is a
source-level break for that scenario only.
- **No behavior change** for callers that don't pass a token: the
pre-v10.2.4 behavior is preserved exactly.

## [10.2.3] - 2026

### Added — AutoChunking (progressive disclosure for large documents)
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ well-reasoned outcomes** rather than single-model guesses.
| **📁 Separate File Output** | Export `result.md`, `statistics.md`, and `logs.md` independently |
| **🔌 Interface-First** | Clean abstractions for providers, factories, builders and executors |
| **🤝 Microsoft.Extensions.AI**| First-class support for `IChatClient` / `IEmbeddingGenerator` — plug in OpenAI, Azure OpenAI, Ollama or any compatible backend, with middleware (function calling, logging) |
| **🛑 Cooperative Cancellation**| Every public async method accepts a `CancellationToken`; a host shutdown or user cancel aborts the debate mid-flight (rounds, LLM calls, MCP tools, RAG, file saves) |
| **🧱 Modern C# 15 (preview)** | Built on .NET 10 with `LangVersion=preview`, file-scoped namespaces, records, span/SIMD hot paths |

---
Expand All @@ -67,6 +68,7 @@ well-reasoned outcomes** rather than single-model guesses.
- [RAG Integration](#-rag-integration)
- [Debate Strategies](#-debate-strategies)
- [Output Files Structure](#-output-files-structure)
- [Cancellation Support](#-cancellation-support)
- [ConsoleApp Examples](#-consoleapp-examples)
- [Installation & Build](#-installation--build)
- [Architecture](#-architecture)
Expand Down Expand Up @@ -632,6 +634,28 @@ await result.SaveLogsAsync("logs.md");

---

## 🛑 Cancellation Support

Every public async method in Delibera honors a `CancellationToken` cooperatively. A single
cancel signal aborts the entire pipeline — rounds, chairman synthesis, LLM calls, MCP tools,
RAG queries and even file writes — via `OperationCanceledException`.

```csharp
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var result = await executor.ExecuteAsync(cts.Token);
```

ASP.NET Core / Worker Services can link the token to `IHostApplicationLifetime.ApplicationStopping`
via the `Delibera.Core.Extensions.CouncilExecutorLifetimeExtensions.ExecuteAsync(executor, lifetime, ct)`
helper. Console apps can wire `Console.CancelKeyPress` to the same `CancellationTokenSource`.

The `Delibera.ConsoleApp` includes a `--cancellation` demo that exercises this end-to-end.

See the [Cancellation section in `Delibera.Core` README](src/Delibera.Core/README.md#-cancellation-support)
for the full table of cancellable operations and the linked-token pattern.

---

## 💻 ConsoleApp Examples

The repository ships with [`Delibera.ConsoleApp`](src/Delibera.ConsoleApp/), a runnable demo
Expand Down
144 changes: 144 additions & 0 deletions src/Delibera.ConsoleApp/Examples/CancellationExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System.Diagnostics;
using Delibera.Core.Council;
using Delibera.Core.Interfaces;
using Delibera.Core.Providers;
using Delibera.Core.Providers.LLM;

namespace Delibera.ConsoleApp.Examples;

/// <summary>
/// Demonstrates cooperative cancellation across the entire Delibera pipeline:
/// Ctrl+C is captured into a <see cref="CancellationTokenSource" /> and the
/// CT is forwarded into <c>executor.ExecuteAsync(cts.Token)</c>, which
/// propagates it through rounds, LLM calls, MCP tools, RAG queries, and
/// even the final result-save step.
/// </summary>
public static class CancellationExample
{
/// <summary>
/// Runs the cancellation example. Press Ctrl+C at any time to abort.
/// </summary>
public static async Task RunAsync()
{
Console.WriteLine("═══════════════════════════════════════════");
Console.WriteLine(" 🛑 Cancellation Example");
Console.WriteLine("═══════════════════════════════════════════");
Console.WriteLine(" Press Ctrl+C at any time to cancel.");
Console.WriteLine(" The debate is wired to a CancellationTokenSource that");
Console.WriteLine(" aborts mid-flight (rounds, LLM calls, file saves).");
Console.WriteLine();

using var cts = new CancellationTokenSource();

// Capture Ctrl+C (and Ctrl+Break on Windows). Cancel the source AND
// suppress the default SIGINT-terminates-process behaviour so we can
// surface a friendly diagnostic instead.
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
if (!cts.IsCancellationRequested)
{
Console.WriteLine("\n⚠️ Ctrl+C detected — signaling cancellation...");
try { cts.Cancel(); } catch (ObjectDisposedException) { /* race */ }
}
};

// 1. Create a provider (Ollama local is the easiest for a demo).
using var factory = new ProviderFactory();
OllamaProvider? ollama = null;
try
{
ollama = factory.CreateLocalOllama("http://localhost:11434");
if (!await ollama.IsAvailableAsync(cts.Token))
{
Console.WriteLine(" ⚠️ Local Ollama not available — falling back to Ollama Cloud (set OLLAMA_API_KEY env).");
ollama.Dispose();
ollama = null;
}
}
catch (Exception ex)
{
Console.WriteLine($" ⚠️ Could not initialise local Ollama: {ex.Message}");
}

if (ollama is null)
{
var apiKey = Environment.GetEnvironmentVariable("OLLAMA_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
Console.WriteLine(" ❌ No Ollama provider available (no local server, no OLLAMA_API_KEY env).");
Console.WriteLine(" Skipping live demo. The cancellation plumbing still works —");
Console.WriteLine(" just run the example against any reachable Ollama endpoint.");
return;
}
ollama = factory.CreateOllama("https://api.ollama.com", apiKey);
}

ILLMProvider llm = ollama; // widen once for the council builder

// 2. Build a small, fast council (1 round, 2 members) so cancellation has a chance to be exercised.
var executor = new CouncilBuilder()
.AddMember("llama2", llm, "Analyst")
.AddMember("qwen2.5", llm, "Strategist")
.SetChairman(Chairman.CreateStandard("qwen2.5", llm))
.WithStandardDebate()
.WithSystemPrompt("You are a thoughtful expert in software engineering.")
.WithUserPrompt("Compare microservices vs a modular monolith for a 10-person startup.")
.WithMaxRounds(1)
.WithTemperature(0.5f)
.SaveResultTo("./debate_results/cancellation_demo.md")
.Build();

executor.OnRoundCompleted += round =>
Console.WriteLine($" ✅ Round {round.RoundNumber} completed in {round.Duration.TotalSeconds:F1}s");
executor.OnLog += e => Console.WriteLine($" ┊ {e}");

// 3. Heartbeat — proves the main thread is alive even while the council is awaiting.
// This task is itself cancellable; it exits cleanly when the user hits Ctrl+C.
using var heartbeatCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
var heartbeat = Task.Run(async () =>
{
var sw = Stopwatch.StartNew();
try
{
while (!heartbeatCts.Token.IsCancellationRequested)
{
Console.WriteLine($" 💓 Still working... ({sw.Elapsed:mm\\:ss}) — press Ctrl+C to cancel");
await Task.Delay(TimeSpan.FromSeconds(2), heartbeatCts.Token);
}
}
catch (OperationCanceledException) { /* expected on shutdown */ }
});

// 4. Run the council with the token.
var swTotal = Stopwatch.StartNew();
try
{
var result = await executor.ExecuteAsync(cts.Token);
swTotal.Stop();

Console.WriteLine($"\n🏆 Debate completed in {swTotal.Elapsed:mm\\:ss}");
Console.WriteLine($" Rounds: {result.Rounds.Count}");
Console.WriteLine($" Verdict chars: {result.FinalVerdict?.Length ?? 0}");

// Also demonstrate that the save methods honor cancellation.
var outputDir = "./debate_results/cancellation_demo";
Directory.CreateDirectory(outputDir);
var (r, s, l) = await result.SaveAllAsync(outputDir, ct: cts.Token);
Console.WriteLine($" Result: {r}");
Console.WriteLine($" Statistics: {s}");
Console.WriteLine($" Logs: {l}");
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
swTotal.Stop();
Console.WriteLine($"\n🛑 Debate cancelled after {swTotal.Elapsed:mm\\:ss}.");
Console.WriteLine(" No partial result was saved (cancellation propagated through all layers).");
}
finally
{
heartbeatCts.Cancel();
try { await heartbeat; } catch { /* ignore */ }
}
}
}
4 changes: 2 additions & 2 deletions src/Delibera.ConsoleApp/Examples/ResilienceExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace Delibera.ConsoleApp.Examples;

/// <summary>
/// Demonstrates Delibera v10.2.2 Polly v8 resilience integration:
/// Demonstrates Delibera v10.2.4 Polly v8 resilience integration:
/// registers <see cref="IDeliberaResiliencePipelineProvider" />, named HttpClients
/// (Ollama.Local, Ollama.Cloud, YandexGPT) with HttpRetryStrategyOptions attached via
/// Microsoft.Extensions.Http.Resilience, and constructs an Ollama provider that uses
Expand All @@ -24,7 +24,7 @@ public static class ResilienceExample
public static async Task RunAsync()
{
Console.WriteLine("═══════════════════════════════════════════");
Console.WriteLine(" 🛡️ Delibera — Polly v8 Resilience Example (v10.2.2)");
Console.WriteLine(" 🛡️ Delibera — Polly v8 Resilience Example (v10.2.4)");
Console.WriteLine("═══════════════════════════════════════════\n");

var configuration = new ConfigurationBuilder()
Expand Down
Loading
Loading