diff --git a/.specify/feature.json b/.specify/feature.json
index 1c74f069f..bc062ce66 100644
--- a/.specify/feature.json
+++ b/.specify/feature.json
@@ -1,3 +1,3 @@
{
- "feature_directory": "specs/010-workflow-json-hardening"
+ "feature_directory": "specs/008-weaver-ai-copilot"
}
diff --git a/AGENTS.md b/AGENTS.md
index 3390d955c..52e58fcd9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -83,7 +83,7 @@ Before handing off changes, verify the following when applicable:
For additional context about technologies to be used, project structure,
-shell commands, and other important information, read `specs/010-workflow-json-hardening/plan.md`.
+shell commands, and other important information, read `specs/008-weaver-ai-copilot/plan.md`.
## Active Technologies
@@ -97,8 +97,11 @@ shell commands, and other important information, read `specs/010-workflow-json-h
- Bounded in-memory recent buffer and bounded subscriber queues by default; no durable database schema. Providers receive redacted content only. (006-diagnostics-console-logs)
- C# latest, nullable reference types enabled, implicit usings enabled. + Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, existing Elsa identity/authorization patterns, Elsa workflow input metadata, `Microsoft.Extensions.Configuration`, `Microsoft.AspNetCore.DataProtection`, EF Core persistence infrastructure, mediator notifications, and optional JavaScript expression integration. (007-secrets-module)
- In-memory store for tests/development; Elsa-managed encrypted store with EF Core persistence for production; configuration-backed read-only store for deployment-managed values. No cloud vault or OS certificate store provider in v1. (007-secrets-module)
+- C# latest, nullable reference types enabled, implicit usings enabled; paired Studio Blazor/Razor module work in the Studio repository. + Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, existing identity/authorization and tenancy services, workflow definition/instance abstractions, diagnostics/log abstractions, `Microsoft.Extensions.Options`, `Microsoft.Extensions.Logging`, OpenTelemetry, `System.Text.Json`, SignalR or SSE streaming, GitHub Copilot SDK isolated behind `Elsa.AI.Copilot`, and headless Copilot CLI JSON-RPC integration. (008-weaver-ai-copilot)
+- Configurable conversation/session retention with in-memory support for development and tests; durable proposal and audit stores required for MVP using Elsa persistence provider abstractions and an EF Core provider package for production. (008-weaver-ai-copilot)
## Recent Changes
+- 008-weaver-ai-copilot: Captures Weaver as a server-hosted, provider-isolated AI copilot platform with Studio chat, governed tools, proposal-only workflow mutations, audit, and extensibility.
- 006-diagnostics-console-logs: Plans raw stdout/stderr console capture with redaction-before-provider boundaries, bounded in-memory recent/live buffers, REST backfill/source endpoints, and a SignalR live hub.
- 005-structured-log-persistence: Plans pluggable structured log storage with in-memory default and opt-in SQLite persistence using FluentMigrator.
- 004-diagnostics-structured-logs: Refactors the unpublished server logs module into diagnostics structured logs and preserves bounded structured `ILogger` capture.
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ddc23de68..cd0f3558d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -141,6 +141,7 @@
+
diff --git a/Elsa.sln b/Elsa.sln
index f28d28813..d01ea75f7 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -381,7 +381,43 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.ConsoleLog
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.StructuredLogs.Dashboard", "src\modules\Elsa.Diagnostics.StructuredLogs.Dashboard\Elsa.Diagnostics.StructuredLogs.Dashboard.csproj", "{215555CD-3B4A-46E7-ACE0-B4ADE1F0FE62}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dashboard", "dashboard", "{E5967DE1-A616-4B47-9024-F0C530EEE712}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext", "src\modules\Elsa.Persistence.VNext\Elsa.Persistence.VNext.csproj", "{A5BE660B-BED0-4D72-BC60-2E6EB42061C0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.Extensions", "src\modules\Elsa.Persistence.VNext.Extensions\Elsa.Persistence.VNext.Extensions.csproj", "{86A93965-54B1-4983-9A87-6E3ADB584BED}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.MongoDb", "src\modules\Elsa.Persistence.VNext.MongoDb\Elsa.Persistence.VNext.MongoDb.csproj", "{E1F2B414-D9E4-4789-8138-3F9DA076FFB6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.PostgreSql", "src\modules\Elsa.Persistence.VNext.PostgreSql\Elsa.Persistence.VNext.PostgreSql.csproj", "{860546C8-60D4-4C11-A1E1-3102969F5E47}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.Relational", "src\modules\Elsa.Persistence.VNext.Relational\Elsa.Persistence.VNext.Relational.csproj", "{E0BF8169-5AB4-4F07-A166-98FE48125359}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.Runtime", "src\modules\Elsa.Persistence.VNext.Runtime\Elsa.Persistence.VNext.Runtime.csproj", "{83836640-31A6-4EDA-AE80-BC926E6B29EC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.Sqlite", "src\modules\Elsa.Persistence.VNext.Sqlite\Elsa.Persistence.VNext.Sqlite.csproj", "{6A17BD0C-8643-4684-BC3F-9E378A467605}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.SqlServer", "src\modules\Elsa.Persistence.VNext.SqlServer\Elsa.Persistence.VNext.SqlServer.csproj", "{2248A649-6CA4-4A1A-8BC3-34257238B560}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.Persistence.VNext", "src\modules\Elsa.Secrets.Persistence.VNext\Elsa.Secrets.Persistence.VNext.csproj", "{807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.UnitTests", "test\unit\Elsa.Persistence.VNext.UnitTests\Elsa.Persistence.VNext.UnitTests.csproj", "{418472CD-1215-4962-9024-F26E9863B5A4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Abstractions", "src\modules\Elsa.AI.Abstractions\Elsa.AI.Abstractions.csproj", "{E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Copilot", "src\modules\Elsa.AI.Copilot\Elsa.AI.Copilot.csproj", "{C9E1429E-BC22-4ABC-820D-08709793ECE7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Host", "src\modules\Elsa.AI.Host\Elsa.AI.Host.csproj", "{24CE63F9-29D1-4243-9144-BB2C1590FCC0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Persistence.EFCore", "src\modules\Elsa.AI.Persistence.EFCore\Elsa.AI.Persistence.EFCore.csproj", "{D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Abstractions.UnitTests", "test\unit\Elsa.AI.Abstractions.UnitTests\Elsa.AI.Abstractions.UnitTests.csproj", "{C597B963-A80F-4093-B3CC-9060854D9D38}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Copilot.UnitTests", "test\unit\Elsa.AI.Copilot.UnitTests\Elsa.AI.Copilot.UnitTests.csproj", "{E087B3BD-D579-422B-A29F-965D33E2C769}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Host.UnitTests", "test\unit\Elsa.AI.Host.UnitTests\Elsa.AI.Host.UnitTests.csproj", "{1C70A220-E69F-434B-966B-9C53A35B072B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Persistence.EFCore.UnitTests", "test\unit\Elsa.AI.Persistence.EFCore.UnitTests\Elsa.AI.Persistence.EFCore.UnitTests.csproj", "{9B74CCC3-9634-4A09-A270-CE1101D96824}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.IntegrationTests", "test\integration\Elsa.AI.IntegrationTests\Elsa.AI.IntegrationTests.csproj", "{DDEA0B6A-A615-4CF8-8B23-F93E390454B2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -1615,6 +1651,234 @@ Global
{215555CD-3B4A-46E7-ACE0-B4ADE1F0FE62}.Release|x64.Build.0 = Release|Any CPU
{215555CD-3B4A-46E7-ACE0-B4ADE1F0FE62}.Release|x86.ActiveCfg = Release|Any CPU
{215555CD-3B4A-46E7-ACE0-B4ADE1F0FE62}.Release|x86.Build.0 = Release|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Debug|x64.Build.0 = Debug|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Debug|x86.Build.0 = Debug|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Release|x64.ActiveCfg = Release|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Release|x64.Build.0 = Release|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Release|x86.ActiveCfg = Release|Any CPU
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0}.Release|x86.Build.0 = Release|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Debug|x64.Build.0 = Debug|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Debug|x86.Build.0 = Debug|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Release|Any CPU.Build.0 = Release|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Release|x64.ActiveCfg = Release|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Release|x64.Build.0 = Release|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Release|x86.ActiveCfg = Release|Any CPU
+ {86A93965-54B1-4983-9A87-6E3ADB584BED}.Release|x86.Build.0 = Release|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Debug|x64.Build.0 = Debug|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Debug|x86.Build.0 = Debug|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Release|x64.ActiveCfg = Release|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Release|x64.Build.0 = Release|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Release|x86.ActiveCfg = Release|Any CPU
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6}.Release|x86.Build.0 = Release|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Debug|x64.Build.0 = Debug|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Debug|x86.Build.0 = Debug|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Release|Any CPU.Build.0 = Release|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Release|x64.ActiveCfg = Release|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Release|x64.Build.0 = Release|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Release|x86.ActiveCfg = Release|Any CPU
+ {860546C8-60D4-4C11-A1E1-3102969F5E47}.Release|x86.Build.0 = Release|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Debug|x64.Build.0 = Debug|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Debug|x86.Build.0 = Debug|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Release|x64.ActiveCfg = Release|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Release|x64.Build.0 = Release|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Release|x86.ActiveCfg = Release|Any CPU
+ {E0BF8169-5AB4-4F07-A166-98FE48125359}.Release|x86.Build.0 = Release|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Debug|x64.Build.0 = Debug|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Debug|x86.Build.0 = Debug|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Release|x64.ActiveCfg = Release|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Release|x64.Build.0 = Release|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Release|x86.ActiveCfg = Release|Any CPU
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC}.Release|x86.Build.0 = Release|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Debug|x64.Build.0 = Debug|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Debug|x86.Build.0 = Debug|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Release|x64.ActiveCfg = Release|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Release|x64.Build.0 = Release|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Release|x86.ActiveCfg = Release|Any CPU
+ {6A17BD0C-8643-4684-BC3F-9E378A467605}.Release|x86.Build.0 = Release|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Debug|x64.Build.0 = Debug|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Debug|x86.Build.0 = Debug|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Release|x64.ActiveCfg = Release|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Release|x64.Build.0 = Release|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Release|x86.ActiveCfg = Release|Any CPU
+ {2248A649-6CA4-4A1A-8BC3-34257238B560}.Release|x86.Build.0 = Release|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Debug|x64.Build.0 = Debug|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Debug|x86.Build.0 = Debug|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Release|x64.ActiveCfg = Release|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Release|x64.Build.0 = Release|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Release|x86.ActiveCfg = Release|Any CPU
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC}.Release|x86.Build.0 = Release|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Debug|x64.Build.0 = Debug|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Debug|x86.Build.0 = Debug|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Release|x64.ActiveCfg = Release|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Release|x64.Build.0 = Release|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Release|x86.ActiveCfg = Release|Any CPU
+ {418472CD-1215-4962-9024-F26E9863B5A4}.Release|x86.Build.0 = Release|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Debug|x64.Build.0 = Debug|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Debug|x86.Build.0 = Debug|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Release|x64.ActiveCfg = Release|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Release|x64.Build.0 = Release|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Release|x86.ActiveCfg = Release|Any CPU
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC}.Release|x86.Build.0 = Release|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Debug|x64.Build.0 = Debug|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Debug|x86.Build.0 = Debug|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Release|x64.ActiveCfg = Release|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Release|x64.Build.0 = Release|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Release|x86.ActiveCfg = Release|Any CPU
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7}.Release|x86.Build.0 = Release|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Debug|x64.Build.0 = Debug|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Debug|x86.Build.0 = Debug|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Release|x64.ActiveCfg = Release|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Release|x64.Build.0 = Release|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Release|x86.ActiveCfg = Release|Any CPU
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0}.Release|x86.Build.0 = Release|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Debug|x64.Build.0 = Debug|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Debug|x86.Build.0 = Debug|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Release|x64.ActiveCfg = Release|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Release|x64.Build.0 = Release|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Release|x86.ActiveCfg = Release|Any CPU
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5}.Release|x86.Build.0 = Release|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Debug|x64.Build.0 = Debug|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Debug|x86.Build.0 = Debug|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Release|x64.ActiveCfg = Release|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Release|x64.Build.0 = Release|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Release|x86.ActiveCfg = Release|Any CPU
+ {C597B963-A80F-4093-B3CC-9060854D9D38}.Release|x86.Build.0 = Release|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Debug|x64.Build.0 = Debug|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Debug|x86.Build.0 = Debug|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Release|x64.ActiveCfg = Release|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Release|x64.Build.0 = Release|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Release|x86.ActiveCfg = Release|Any CPU
+ {E087B3BD-D579-422B-A29F-965D33E2C769}.Release|x86.Build.0 = Release|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Debug|x64.Build.0 = Debug|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Debug|x86.Build.0 = Debug|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Release|x64.ActiveCfg = Release|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Release|x64.Build.0 = Release|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Release|x86.ActiveCfg = Release|Any CPU
+ {1C70A220-E69F-434B-966B-9C53A35B072B}.Release|x86.Build.0 = Release|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Debug|x64.Build.0 = Debug|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Debug|x86.Build.0 = Debug|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Release|x64.ActiveCfg = Release|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Release|x64.Build.0 = Release|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Release|x86.ActiveCfg = Release|Any CPU
+ {9B74CCC3-9634-4A09-A270-CE1101D96824}.Release|x86.Build.0 = Release|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Debug|x64.Build.0 = Debug|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Debug|x86.Build.0 = Debug|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|x64.ActiveCfg = Release|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|x64.Build.0 = Release|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|x86.ActiveCfg = Release|Any CPU
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1749,13 +2013,31 @@ Global
{E4B1546A-3F35-4B5C-815C-07E58B00163F} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
{E18931B2-82EE-4398-B38A-7B8B449957CD} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
{729FD075-2863-48AB-A3C0-A4F7A9277263} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
+ {4FD4C59B-2804-4F6B-AD38-2562CC01C510} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
{157EDBA7-B04F-4EA0-8377-434D1065ACF6} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {27300F17-F959-4D4A-885F-FDDBE81456C1} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
+ {05AC1253-922A-4BC4-93D0-765752026B29} = {B08B4E00-C2AB-48F3-8389-449F42AEF179}
{D1212632-20D4-4115-9E8F-05EEEC4604BE} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
{215555CD-3B4A-46E7-ACE0-B4ADE1F0FE62} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
- {E5967DE1-A616-4B47-9024-F0C530EEE712} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
- {05AC1253-922A-4BC4-93D0-765752026B29} = {B08B4E00-C2AB-48F3-8389-449F42AEF179}
- {27300F17-F959-4D4A-885F-FDDBE81456C1} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
- {4FD4C59B-2804-4F6B-AD38-2562CC01C510} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
+ {A5BE660B-BED0-4D72-BC60-2E6EB42061C0} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {86A93965-54B1-4983-9A87-6E3ADB584BED} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {E1F2B414-D9E4-4789-8138-3F9DA076FFB6} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {860546C8-60D4-4C11-A1E1-3102969F5E47} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {E0BF8169-5AB4-4F07-A166-98FE48125359} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {83836640-31A6-4EDA-AE80-BC926E6B29EC} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {6A17BD0C-8643-4684-BC3F-9E378A467605} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {2248A649-6CA4-4A1A-8BC3-34257238B560} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {418472CD-1215-4962-9024-F26E9863B5A4} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {C9E1429E-BC22-4ABC-820D-08709793ECE7} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {24CE63F9-29D1-4243-9144-BB2C1590FCC0} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {C597B963-A80F-4093-B3CC-9060854D9D38} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {E087B3BD-D579-422B-A29F-965D33E2C769} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {1C70A220-E69F-434B-966B-9C53A35B072B} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {9B74CCC3-9634-4A09-A270-CE1101D96824} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {DDEA0B6A-A615-4CF8-8B23-F93E390454B2} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/design/persistence-vnext-poc.md b/design/persistence-vnext-poc.md
new file mode 100644
index 000000000..0b96d8255
--- /dev/null
+++ b/design/persistence-vnext-poc.md
@@ -0,0 +1,94 @@
+# Persistence vNext POC
+
+## Goal
+
+This proof of concept explores whether Elsa modules can declare persistence intent once and let database providers own physical schema materialization. The immediate target is reducing the maintenance cost of per-module EF Core migrations across SQLite, SQL Server, PostgreSQL, MySQL, and Oracle.
+
+## Current Pain
+
+The existing EF Core stack keeps logical module configuration in module packages, while migrations live in provider packages. That works, but every schema change fans out into provider-specific migrations and model snapshots. The Secrets module is a compact example: a single logical `Secrets` table is represented by one EF model configuration and five provider-specific initial migrations.
+
+## Proposed Model
+
+The POC introduces four layers:
+
+1. Module descriptors: modules expose `IPersistenceSchemaProvider` and return versioned provider-neutral storage-unit, field, key, and index descriptors.
+2. Provider-family planning: a planner turns the same descriptors into a family-specific plan. The POC has relational and document planners.
+3. Provider rendering/materialization: a provider renders or executes physical operations. The POC implements SQLite SQL rendering/execution and SQL Server render-only output.
+4. Schema history: a provider records applied descriptor versions so startup materialization can be idempotent.
+
+The core extension points are intentionally small:
+
+- `IPersistenceSchemaProvider`
+- `IPersistenceSchemaPlanner`
+- `IPersistenceSchemaRenderer`
+- `IPersistenceSchemaMigrator`
+
+In this model, the module owns storage intent:
+
+- storage unit name
+- namespace
+- schema version
+- fields
+- logical types
+- nullability
+- lengths
+- keys
+- indexes
+
+The provider owns physical details:
+
+- whether a storage unit becomes a table, collection, bucket, container, or something else
+- SQL type names
+- identifier quoting
+- whether schemas are supported
+- document collection/index options
+- BSON/JSON naming conventions
+- generated DDL or migration operations
+- execution strategy
+- schema history storage
+
+## POC Scope
+
+The POC models the Secrets storage unit through `SecretPersistenceSchemaProvider`. It does not replace the current EF Core repository or migrations. Tests prove that the Secrets module can declare storage intent once, SQLite can render and execute a relational table, SQL Server can render provider-specific relational DDL from the same descriptor, a document planner can produce a collection/index plan, and SQLite can record applied schema versions.
+
+SQLite records versions in `ElsaSchemaVersions`:
+
+- `SchemaName`
+- `Version`
+- `AppliedAt`
+
+`SqliteSchemaVersionRunner` creates the history table if needed, checks the highest version for the descriptor schema name, executes rendered DDL only when the descriptor version is newer, and records the applied version in the same transaction.
+
+## Document Databases
+
+Document database providers should not consume relational table descriptors directly. They should consume the provider-neutral storage-unit descriptors and produce a document plan:
+
+- collection name
+- namespace/database/tenant partition where applicable
+- document fields
+- identity field
+- secondary indexes
+- unique indexes
+- provider-specific options such as TTL, collation, shard keys, or JSON/BSON schema validation
+
+MongoDB, for example, would implement a planner/materializer that maps `PersistenceStorageUnit` to collection and index creation. Data-shape changes would be handled by versioned migration operations, not by relational DDL.
+
+## General-Purpose Reuse
+
+This could be extracted into a reusable framework, but the reusable boundary should be below Elsa-specific module wiring:
+
+- a core package for descriptors, version metadata, planning interfaces, and migration result contracts
+- provider-family packages for relational and document planning primitives
+- provider packages for SQLite, SQL Server, MongoDB, PostgreSQL, etc.
+- optional host integration packages for Elsa, ASP.NET Core, or worker services
+
+The reusable core should not depend on Elsa features, EF Core, CShells, or any specific database driver. Elsa would then be a consumer that registers schema providers from modules and selects provider implementations.
+
+## Known Gaps
+
+This POC intentionally does not solve schema diffing, data migrations, provider-specific advanced indexes, foreign keys, optimistic concurrency, or integration with Elsa shell features. The descriptor now includes a schema version and SQLite consumes it, but the runner applies the current full descriptor rather than a chain of per-version operations.
+
+## Viability
+
+The approach looks viable for simple module-owned storage units and indexes. Rendering both SQLite and SQL Server plus planning a document collection from the same Secrets descriptor suggests the module/provider boundary is useful. The SQLite version runner shows that provider-owned materialization can be idempotent without EF Core migrations. The main challenge remains evolution: Elsa would need a versioned descriptor/migration story so providers can apply each version safely, including non-idempotent changes and data backfills, before attempting automatic diffs.
diff --git a/design/persistence-vnext-roadmap.md b/design/persistence-vnext-roadmap.md
new file mode 100644
index 000000000..7a653b5a5
--- /dev/null
+++ b/design/persistence-vnext-roadmap.md
@@ -0,0 +1,535 @@
+# Persistence vNext Roadmap
+
+## Vision
+
+Persistence vNext should let modular applications declare persistence intent once and let database providers materialize that intent using the best physical model available to them.
+
+For Elsa, this means modules can contribute storage manifests, indexes, and runtime entity definitions without owning SQL Server, SQLite, PostgreSQL, Oracle, MySQL, MongoDB, or EF Core migration projects.
+
+The long-term model is:
+
+- portable by default
+- provider-optimized when needed
+- document-first for dynamic/runtime entities
+- explicit-index driven for portable queries
+- extensible enough for relational and document databases
+- independent enough to become reusable outside Elsa
+
+## Non-Goals
+
+Persistence vNext should not become:
+
+- a universal ORM
+- a LINQ provider
+- an EF Core replacement for all data access
+- a lowest-common-denominator database API
+- a reporting/BI engine
+- a magic cross-database query translator
+
+Its job is to own modular persistence manifests, schema/index materialization, versioning, and a focused document/index query model.
+
+## Design Principles
+
+1. Modules declare intent, providers own physical shape.
+2. Schema authority is the manifest, not EF Core migrations.
+3. Runtime-defined entities use document storage, not table-per-entity by default.
+4. Portable queries run through declared indexes.
+5. Advanced provider behavior is exposed through capabilities and opt-in policies.
+6. Provider-specific escape hatches are acceptable when explicit.
+7. The core abstractions must not depend on Elsa, EF Core, MongoDB, or any one provider.
+8. Start with simple, reliable append-only versioning before automatic diffing.
+9. Optimize hot paths without sacrificing the default portable path.
+
+## Target Package Shape
+
+### General-Purpose Packages
+
+- `ModularPersistence.Abstractions`
+ - storage manifests
+ - storage units
+ - fields
+ - indexes
+ - versions
+ - provider capabilities
+ - planner, renderer, migrator contracts
+
+- `ModularPersistence.Document`
+ - document descriptors
+ - collection/index plans
+ - portable document session abstractions
+ - index-first query model
+
+- `ModularPersistence.Relational`
+ - relational plans
+ - SQL operation model
+ - relational schema history conventions
+ - SQL execution abstractions
+
+- `ModularPersistence.Sqlite`
+ - SQLite type mapping
+ - SQL rendering
+ - schema history
+ - portable document store implementation
+
+- `ModularPersistence.SqlServer`
+ - SQL Server type mapping
+ - SQL rendering
+ - schema history
+ - optimized index strategies
+
+- `ModularPersistence.PostgreSql`
+ - PostgreSQL type mapping
+ - JSONB options
+ - expression/generated indexes where useful
+
+- `ModularPersistence.MongoDb`
+ - collection/index planning
+ - native MongoDB indexes
+ - document session implementation
+ - optional schema validation
+
+### Elsa Packages
+
+- `Elsa.Persistence.VNext`
+ - Elsa module integration
+ - shell feature wiring
+ - options
+ - module discovery
+ - startup migration orchestration
+
+- `Elsa.Persistence.VNext.RuntimeEntities`
+ - runtime entity definitions
+ - admin-defined schema lifecycle
+ - generated API/UI integration points
+ - permissions and audit hooks
+
+## Core Concepts
+
+### Storage Manifest
+
+A module or runtime admin defines:
+
+- schema name
+- schema version
+- storage units
+- fields
+- keys
+- indexes
+- validation metadata
+- provider requirements
+- optional physicalization policy
+
+### Storage Unit
+
+A storage unit is provider-neutral. Providers map it differently:
+
+- relational: table or document table/index table
+- MongoDB: collection or shared collection
+- Cosmos DB: container/partition strategy
+- in-memory: dictionary plus indexes
+
+### Document Store
+
+Runtime entities and many module aggregates should be stored as documents:
+
+- `Id`
+- `Type`
+- `TenantId`
+- `Version`
+- `CreatedAt`
+- `UpdatedAt`
+- `Data`
+- optional metadata
+
+### Indexes
+
+Indexes are the portable query surface.
+
+Portable queries should only target declared indexes. Providers decide how to implement them:
+
+- relational generic field index table
+- relational typed index table
+- SQL generated/computed column
+- MongoDB native index
+- in-memory lookup table
+
+### Physicalization Policy
+
+Each storage unit or index can choose a policy:
+
+- `PortableDocument`
+ - safest default
+ - shared document table/collection plus generic indexes
+
+- `OptimizedIndexes`
+ - provider creates stronger typed/materialized indexes
+ - still document-backed
+
+- `NativePhysicalized`
+ - provider may create typed tables, collection-per-entity, generated columns, JSONB expression indexes, etc.
+ - opt-in for hot paths
+
+## Roadmap
+
+## Phase 0: Research And Validation
+
+Goal: validate the design against Elsa's real persistence surface.
+
+Tasks:
+
+- Inventory current EF Core modules, stores, migrations, and provider packages.
+- Classify stores into document-friendly, relational-specific, and high-performance hot paths.
+- Identify query patterns per module.
+- Compare YesSQL, Marten, MongoDB, EF Core, FluentMigrator, Liquibase, and Orchard Core data migrations.
+- Decide which Elsa module is the first production candidate.
+
+Exit criteria:
+
+- Persistence inventory completed.
+- First production module candidate selected.
+- Provider priorities selected.
+
+Recommended first candidates:
+
+- Secrets
+- Labels
+- Tenants
+- simple diagnostics stores
+
+Avoid first:
+
+- workflow runtime hot paths
+- workflow instance queries
+- execution log history
+
+## Phase 1: Extract Clean Core Abstractions
+
+Goal: turn the POC into clean reusable contracts.
+
+Tasks:
+
+- Rename POC concepts into neutral package names.
+- Separate core abstractions from Elsa.
+- Finalize descriptor vocabulary:
+ - manifest
+ - storage unit
+ - field
+ - key
+ - index
+ - version
+ - capability
+- Add provider capability checks.
+- Add validation errors and diagnostics.
+- Add contract tests for descriptors and planners.
+
+Exit criteria:
+
+- No Elsa dependencies in core packages.
+- Descriptors support relational and document planning.
+- Provider capability validation exists.
+- SQLite and SQL Server render tests still pass.
+
+## Phase 2: Portable Document Store MVP
+
+Goal: implement a YesSQL-like portable document store over SQLite first.
+
+Tasks:
+
+- Define `IDocumentStore`.
+- Define document envelope:
+ - `Id`
+ - `StorageUnit`
+ - `Version`
+ - `Content`
+ - timestamps
+- Define explicit save and query request records.
+- Implement SQLite document table.
+- Implement generic field index table.
+- Implement save/load/delete.
+- Implement index maintenance in the same transaction.
+- Implement basic optimistic concurrency.
+
+Exit criteria:
+
+- Can store and load documents.
+- Can maintain declared indexes.
+- Can query by declared indexes.
+- Startup materialization is idempotent.
+
+## Phase 3: Portable Query Model
+
+Goal: define useful queries without becoming an ORM.
+
+Tasks:
+
+- Define index-first query API.
+- Support:
+ - equals
+ - not equals
+ - in
+ - range
+ - starts-with where supported
+ - null checks
+ - sorting
+ - paging
+- Add provider capability validation for unsupported operations.
+- Add query-plan diagnostics.
+- Add tests ensuring unindexed portable queries fail clearly.
+
+Exit criteria:
+
+- Modules can query common indexes portably.
+- Unsupported query shapes produce clear errors.
+- Query API does not expose provider-specific behavior by accident.
+
+## Phase 4: SQL Server And PostgreSQL Providers
+
+Goal: prove the relational provider model across serious relational engines.
+
+Tasks:
+
+- Implement SQL Server document store.
+- Implement PostgreSQL document store.
+- Support generic indexes.
+- Add provider-specific optimized index options:
+ - SQL Server computed columns where useful
+ - PostgreSQL JSONB/expression indexes where useful
+- Add transaction and locking strategy.
+- Add provider integration tests.
+
+Exit criteria:
+
+- Same document/index tests pass on SQLite, SQL Server, and PostgreSQL.
+- Provider differences are isolated to provider packages.
+- Startup race conditions are handled.
+
+## Phase 5: MongoDB Provider
+
+Goal: prove native document database support.
+
+Tasks:
+
+- Implement MongoDB document store.
+- Decide shared collection versus collection-per-type defaults.
+- Map storage units to collections.
+- Map declared indexes to MongoDB indexes.
+- Add optimistic concurrency.
+- Add basic transaction support when available.
+- Add provider capability checks for unsupported relational assumptions.
+
+Exit criteria:
+
+- Same document/index tests pass on MongoDB.
+- MongoDB uses native indexes.
+- No relational tables/index assumptions leak into the document provider.
+
+## Phase 6: Elsa Integration
+
+Goal: consume the general packages from Elsa.
+
+Tasks:
+
+- Add Elsa-specific registration package.
+- Let modules register manifests through module features.
+- Add startup materialization service.
+- Add shell feature support.
+- Add options for selected provider and connection settings.
+- Add diagnostics endpoint for applied manifests and provider status.
+
+Exit criteria:
+
+- An Elsa module can declare storage intent without EF Core.
+- Elsa startup materializes the selected provider.
+- Applied versions are observable.
+
+## Phase 7: First Elsa Module Migration
+
+Goal: migrate one real module safely.
+
+Recommended module: Secrets.
+
+Tasks:
+
+- Implement Secrets document store.
+- Preserve existing API behavior.
+- Add import path from EF Core Secrets tables if needed.
+- Add dual-read or explicit migration utility if required.
+- Add provider tests for SQLite, SQL Server, and MongoDB.
+
+Exit criteria:
+
+- Secrets works without EF Core migrations.
+- Existing tests pass.
+- Provider-specific persistence packages are no longer needed for Secrets vNext.
+
+## Phase 8: Runtime-Defined Entities
+
+Goal: let admins define entities at runtime.
+
+Tasks:
+
+- Define `RuntimeEntityDefinition`.
+- Add draft/published/retired lifecycle.
+- Add validation rules.
+- Add index definitions.
+- Add provider capability checks before publish.
+- Materialize published definitions.
+- Generate CRUD endpoints.
+- Expose admin UI integration points.
+- Add audit trail for schema changes.
+
+Exit criteria:
+
+- Admin can define an entity.
+- System validates and publishes it.
+- Data can be created, queried by indexed fields, updated, and deleted.
+- No physical table per entity is required by default.
+
+## Phase 9: Physicalization And Performance
+
+Goal: optimize hot entities and indexes without giving up portability.
+
+Tasks:
+
+- Add storage policy:
+ - `PortableDocument`
+ - `OptimizedIndexes`
+ - `NativePhysicalized`
+- Add provider-specific physicalization planners.
+- Add operational commands to promote/demote indexes.
+- Add benchmarks.
+- Add guidance for when to physicalize.
+
+Exit criteria:
+
+- Portable defaults remain simple.
+- Hot-path indexes can be optimized.
+- Provider-specific behavior is explicit and observable.
+
+Local progress note: the POC now includes provider-neutral physicalization policies with schema validation plus SQLite and MongoDB planners. SQLite emits dedicated table/index DDL operations; MongoDB emits dedicated collection/native-index operations. This proves the extension point, but representative benchmarks and operational promote/demote commands are still needed.
+
+## Phase 10: Workflow Runtime Evaluation
+
+Goal: determine whether high-throughput workflow runtime persistence should use vNext.
+
+Tasks:
+
+- Benchmark workflow instance queries.
+- Benchmark bookmark and trigger queries.
+- Evaluate lock/concurrency semantics.
+- Evaluate event/log volume storage.
+- Decide where specialized persistence remains better.
+
+Exit criteria:
+
+- Clear decision for runtime stores.
+- No premature migration of hot workflow internals.
+
+Likely outcome:
+
+- Management/catalog/metadata stores are good candidates.
+- Runtime execution hot paths may need specialized providers or optimized physicalization.
+
+Local progress note: the POC includes a documented go/no-go decision and evaluator. The current recommendation is to use vNext for management/module metadata first, gate hot lookups behind benchmarks and physicalization, and keep queues/logs on specialized providers until runtime contract tests prove otherwise.
+
+## Phase 11: Hardening
+
+Goal: make the framework production-safe.
+
+Tasks:
+
+- Startup locking.
+- Transaction strategy.
+- Retry strategy.
+- Idempotency guarantees.
+- Version history repair tools.
+- Provider diagnostics.
+- Backup/restore guidance.
+- Multi-tenant isolation model.
+- Security and permission model for runtime schemas.
+- Roll-forward migration guidance.
+
+Exit criteria:
+
+- Safe concurrent startup.
+- Clear recovery story.
+- Provider failures are diagnosable.
+
+## Phase 12: General-Purpose OSS Extraction
+
+Goal: publish the reusable framework outside Elsa.
+
+Tasks:
+
+- Finalize package names.
+- Remove Elsa-specific concepts.
+- Add examples:
+ - ASP.NET Core modular monolith
+ - SQLite
+ - SQL Server
+ - MongoDB
+- Add documentation.
+- Add provider compatibility matrix.
+- Define semantic versioning policy.
+
+Exit criteria:
+
+- Elsa consumes the general packages.
+- Non-Elsa sample app works.
+- Public API is stable enough for external users.
+
+## Migration Strategy For Elsa
+
+1. Keep current EF Core persistence intact.
+2. Introduce vNext side-by-side.
+3. Use vNext for new experimental modules first.
+4. Migrate small modules next.
+5. Provide import utilities for existing data.
+6. Avoid dual-write unless absolutely necessary.
+7. Do not migrate runtime hot paths until benchmarks justify it.
+
+## Major Risks
+
+- Query abstraction grows too large.
+- Provider capabilities become hard to reason about.
+- Document indexes become expensive on relational providers.
+- Runtime-defined entities introduce product/security complexity.
+- Automatic diffs become unreliable.
+- Physicalization policies create operational burden.
+- MongoDB and relational transaction semantics diverge.
+
+## Key Decisions To Make Early
+
+- Package naming and ownership.
+- Whether this starts as Elsa-internal or separate repo.
+- First production Elsa module.
+- Provider priority order.
+- Whether PostgreSQL or SQL Server follows SQLite.
+- MongoDB shared collection versus collection-per-type default.
+- How much query expressiveness is allowed in portable mode.
+- Runtime entity UI scope.
+
+## Suggested Execution Order
+
+1. Finish core abstractions in the POC.
+2. Build SQLite document store.
+3. Convert Secrets to use the document store.
+4. Add SQL Server provider.
+5. Add MongoDB provider.
+6. Add Elsa integration.
+7. Add runtime-defined entities.
+8. Add physicalization policies.
+9. Decide whether to extract as OSS.
+
+## Success Criteria
+
+This initiative succeeds if:
+
+- modules declare persistence once
+- provider-specific migrations are no longer required per module
+- relational and MongoDB providers both work
+- runtime-defined entities can be stored without table-per-entity
+- portable queries are predictable and index-driven
+- hot paths have opt-in physicalization
+- Elsa remains database agnostic
+- the reusable core stays small enough to understand
diff --git a/design/persistence-vnext-workflow-runtime-evaluation.md b/design/persistence-vnext-workflow-runtime-evaluation.md
new file mode 100644
index 000000000..267871db4
--- /dev/null
+++ b/design/persistence-vnext-workflow-runtime-evaluation.md
@@ -0,0 +1,85 @@
+# Persistence vNext Workflow Runtime Evaluation
+
+## Decision
+
+Persistence vNext is viable for Elsa management, catalog, metadata, admin-defined entity, and small module stores. It should not replace workflow runtime hot-path stores yet.
+
+The current go/no-go decision is:
+
+- Go: workflow definitions, metadata stores, secrets, labels, tenants, admin-defined entities, and other document-friendly module data.
+- Conditional go: workflow instance management/query surfaces if provider benchmarks prove the declared indexes are fast enough, preferably with physicalization policies for common filters.
+- No-go for now: bookmarks, triggers, bookmark queues, dispatch/outbox queues, activity execution records, and workflow execution logs.
+
+## Why Runtime Stores Are Different
+
+Workflow runtime stores are not only persistence concerns. They encode runtime semantics:
+
+- bookmark and trigger lookup latency
+- distributed locking
+- queue visibility, retry, and dead-letter behavior
+- append-heavy log/event volume
+- cleanup and retention behavior
+- concurrency under multiple workers
+
+A portable document/index layer can represent these records, but representation alone is not enough. The provider must also prove that runtime semantics are correct and fast under load.
+
+## Recommended Runtime Strategy
+
+Keep specialized runtime providers until three conditions are met:
+
+1. Representative benchmarks exist for SQLite, SQL Server, PostgreSQL, and MongoDB.
+2. Provider contract tests validate concurrency, locking, retry, and cleanup behavior.
+3. Hot-path storage units have explicit physicalization policies with rollback guidance.
+
+Use Persistence vNext first for stores where the risk profile is lower:
+
+- management/catalog data
+- module configuration/state
+- runtime-defined entities
+- operational metadata
+- queryable admin data
+
+## Physicalization Requirement
+
+If workflow instances or other high-volume records move to vNext, they should use portable documents by default in development, but production providers should be able to opt into:
+
+- relational dedicated tables
+- relational generated or typed index columns
+- MongoDB dedicated collections
+- native provider indexes
+- retention/cleanup commands
+
+This keeps module intent provider-neutral while letting providers own the physical performance shape.
+
+## Required Evidence Before Migrating Hot Paths
+
+Benchmarks should cover:
+
+- workflow instance queries by status, sub-status, definition id, version, correlation id, and updated timestamp
+- bookmark lookup by activity type, hash, workflow instance id, and tenant id
+- trigger lookup by hash, workflow definition id, and tenant id
+- queue dequeue/lease/retry/dead-letter behavior
+- log append and retention cleanup throughput
+
+Contract tests should cover:
+
+- optimistic concurrency failures
+- distributed lock behavior
+- duplicate startup materialization
+- worker retry behavior
+- transactional index updates
+- recovery after partial materialization failure
+
+## Migration Strategy
+
+1. Keep current EF Core runtime persistence intact.
+2. Add vNext-backed stores only for low-risk module and management data.
+3. Add benchmark fixtures for workflow runtime query shapes.
+4. Add provider contract tests for locking, queues, and append-heavy logs.
+5. Introduce physicalization policies for workflow instance and bookmark indexes.
+6. Pilot one non-critical runtime-adjacent store.
+7. Only migrate runtime hot paths after benchmarks and contract tests pass across the supported providers.
+
+## POC Status
+
+The POC now includes a small evaluator that codifies this decision. It classifies metadata stores as vNext candidates, hot distributed lookups as benchmark-gated, and queue/log workloads as specialized-provider territory for now.
diff --git a/specs/008-weaver-ai-copilot/checklists/requirements.md b/specs/008-weaver-ai-copilot/checklists/requirements.md
new file mode 100644
index 000000000..789a09abc
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/checklists/requirements.md
@@ -0,0 +1,34 @@
+# Specification Quality Checklist: Weaver AI Copilot Platform
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-05-20
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- Validation passed after initial drafting. Provider/runtime specifics are intentionally deferred to [plan.md](../plan.md) and design contracts.
diff --git a/specs/008-weaver-ai-copilot/contracts/rest-api.md b/specs/008-weaver-ai-copilot/contracts/rest-api.md
new file mode 100644
index 000000000..3a5a362b7
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/contracts/rest-api.md
@@ -0,0 +1,145 @@
+# REST and Streaming API Contract: Weaver AI Copilot Platform
+
+## POST `/ai/chat`
+
+Starts or continues a Weaver chat turn.
+
+**Request**
+
+```json
+{
+ "conversationId": "optional-existing-conversation-id",
+ "message": "Explain why this workflow failed",
+ "attachments": [
+ {
+ "kind": "WorkflowInstance",
+ "referenceId": "instance-123",
+ "timeRange": {
+ "from": "2026-05-20T08:00:00Z",
+ "to": "2026-05-20T10:00:00Z"
+ }
+ }
+ ],
+ "agent": "instance-diagnostics"
+}
+```
+
+**Response**
+
+Streams `AIStreamEvent` records as Server-Sent Events or an equivalent server-owned streaming transport.
+
+```json
+{
+ "type": "assistant.delta",
+ "conversationId": "conversation-123",
+ "sequence": 12,
+ "timestamp": "2026-05-20T10:15:00Z",
+ "data": {
+ "messageId": "message-456",
+ "content": "The failure happened in..."
+ }
+}
+```
+
+**Stream Event Types**
+
+- `conversation.started`
+- `assistant.delta`
+- `assistant.completed`
+- `tool.started`
+- `tool.progress`
+- `tool.result`
+- `proposal.created`
+- `proposal.updated`
+- `conversation.completed`
+- `conversation.error`
+
+## GET `/ai/tools`
+
+Returns available tools for the current user, tenant, and optional agent scope.
+
+**Query**: `agent`
+
+**Response**
+
+```json
+[
+ {
+ "name": "workflow.getDefinition",
+ "displayName": "Get workflow definition",
+ "mutability": "ReadOnly",
+ "dangerLevel": "Low",
+ "permissions": ["read:workflows"],
+ "tenantBehavior": "TenantScoped",
+ "auditBehavior": "RecordInvocation",
+ "schema": {}
+ }
+]
+```
+
+## GET `/ai/capabilities`
+
+Returns provider-neutral Weaver capabilities for Studio.
+
+**Response**
+
+```json
+{
+ "streaming": true,
+ "conversationPersistence": true,
+ "proposalReview": true,
+ "supportedAttachmentKinds": [
+ "WorkflowDefinition",
+ "WorkflowInstance"
+ ],
+ "agents": [
+ {
+ "name": "workflow-author",
+ "displayName": "Workflow author",
+ "description": "Creates safe workflow proposals"
+ }
+ ]
+}
+```
+
+## GET `/ai/proposals/{id}`
+
+Returns proposal details when the current user can view the referenced workflow or target area.
+
+## POST `/ai/proposals/{id}/approve`
+
+Marks a proposal as approved by an authorized user. Approval does not apply the proposal. In MVP, the same authorized user may request, approve, and apply the proposal.
+
+## POST `/ai/proposals/{id}/reject`
+
+Rejects a proposal and records the reason.
+
+## POST `/ai/proposals/{id}/apply`
+
+Validates and applies an approved proposal.
+
+**Apply Rules**
+
+- User must have apply permission.
+- Proposal must be approved.
+- Proposal baseline must match current workflow revision when applicable.
+- Validation must pass.
+- Apply emits audit records and returns the created or updated workflow reference.
+
+## Streaming Reconnect
+
+If the streaming client disconnects during a turn, the server continues the turn for a configurable grace window. Authorized clients may reconnect with the conversation ID and recover durable outputs produced during the grace window.
+
+## Runtime Trend Scope
+
+Runtime trend tools inspect attached references plus an explicit user-selected time range and diagnostics scope by default. Tenant-wide runtime analysis requires a future explicit scope expansion.
+
+## Error Behavior
+
+- `400`: Invalid request, unsupported attachment kind, invalid proposal transition.
+- `401`: Unauthenticated.
+- `403`: Missing permission, tenant mismatch, or denied tool/proposal access.
+- `404`: Context reference, conversation, or proposal not found.
+- `409`: Stale proposal baseline or invalid proposal state.
+- `422`: Proposal validation failed.
+- `503`: Provider runtime unavailable.
diff --git a/specs/008-weaver-ai-copilot/contracts/runtime-contract.md b/specs/008-weaver-ai-copilot/contracts/runtime-contract.md
new file mode 100644
index 000000000..a05bf9ba7
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/contracts/runtime-contract.md
@@ -0,0 +1,81 @@
+# Runtime Contract: Weaver AI Copilot Platform
+
+## Core Abstractions
+
+```csharp
+public interface IAIProvider
+{
+ string Name { get; }
+ ValueTask CreateSessionAsync(CreateAISessionRequest request, CancellationToken cancellationToken = default);
+ IAsyncEnumerable ExecuteTurnAsync(AITurnRequest request, CancellationToken cancellationToken = default);
+}
+
+public interface IAIOrchestrator
+{
+ IAsyncEnumerable ExecuteChatAsync(AIChatRequest request, CancellationToken cancellationToken = default);
+}
+
+public interface IAITool
+{
+ AIToolDefinition Definition { get; }
+ ValueTask ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellationToken = default);
+}
+
+public interface IAIToolRegistry
+{
+ ValueTask> ListAsync(AIToolQuery query, CancellationToken cancellationToken = default);
+ ValueTask FindAsync(string name, AIToolQuery query, CancellationToken cancellationToken = default);
+}
+
+public interface IAIContextProvider
+{
+ string Kind { get; }
+ ValueTask ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default);
+}
+
+public interface IAIProposalStore
+{
+ ValueTask FindAsync(string id, string? tenantId, CancellationToken cancellationToken = default);
+ ValueTask SaveAsync(AIProposal proposal, CancellationToken cancellationToken = default);
+}
+
+public interface IAIAuditSink
+{
+ ValueTask RecordAsync(AIAuditEvent auditEvent, CancellationToken cancellationToken = default);
+}
+```
+
+`IAIProposalStore` and `IAIAuditSink` require durable production implementations for MVP. Conversation/session retention remains configurable and may use in-memory storage for development and tests.
+
+## Provider Boundary
+
+- `Elsa.AI.Abstractions` owns `AIProviderEvent`, `AIStreamEvent`, session, tool, context, proposal, and audit models.
+- `Elsa.AI.Copilot` maps Copilot SDK and CLI events into Elsa-owned models.
+- No Copilot SDK type may appear in `Elsa.AI.Abstractions`, `Elsa.AI.Host`, workflow models, REST contracts, or Studio contracts.
+
+## Built-In MVP Tools
+
+| Tool | Mutability | Purpose |
+|------|------------|---------|
+| `workflow.getDefinition` | Read-only | Retrieve an authorized workflow definition summary and graph payload. |
+| `workflow.listDefinitions` | Read-only | Search authorized workflow definitions. |
+| `instance.get` | Read-only | Retrieve an authorized workflow instance summary and state. |
+| `instance.search` | Read-only | Search authorized workflow instances by status, workflow, and time range. |
+| `activities.catalog` | Read-only | Retrieve activity descriptors available to the tenant. |
+| `workflow.proposeCreate` | Proposal | Create a workflow proposal from structured draft output. |
+| `workflow.proposeUpdate` | Proposal | Create an update proposal from a baseline and structured patch. |
+| `workflow.validateDraft` | Proposal | Validate a structured workflow draft without persisting it. |
+
+## Tool Execution Rules
+
+- Validate tenant, RBAC, ownership, danger level, global enablement, and agent scope before execution.
+- Resolve persistence through Elsa service abstractions only.
+- Redact arguments and results before stream and audit output.
+- Clamp result size before model context inclusion.
+- Record success, failure, and denial through `IAIAuditSink`.
+
+## Runtime Scope Rules
+
+- In-progress chat turns continue for a configurable disconnect grace window and expose recoverable durable outputs to authorized reconnecting clients.
+- Runtime trend analysis is limited by default to attached references plus an explicit time range and diagnostics scope.
+- The same authorized user may request, approve, reject, and apply proposals in MVP.
diff --git a/specs/008-weaver-ai-copilot/contracts/studio-contract.md b/specs/008-weaver-ai-copilot/contracts/studio-contract.md
new file mode 100644
index 000000000..c5540666a
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/contracts/studio-contract.md
@@ -0,0 +1,65 @@
+# Studio Contract: Weaver AI Copilot Platform
+
+## Studio Responsibilities
+
+- Render the Weaver chat panel.
+- Send user messages and context references to Elsa Server.
+- Render streaming assistant, tool, and proposal events.
+- Render proposal details, graph preview, graph diff, warnings, and validation diagnostics.
+- Initiate approve, reject, and apply actions through server APIs.
+- Persist UI state only as needed for user navigation.
+
+## Prototype Reference
+
+Before implementing `Elsa.Studio.AI`, review the `elsa-extensions` `origin/feat/ai` prototype at commit `93f0e09d71e57f5daff1e2d593f0a51faaa80417` and its parents. Carry forward route/menu/table/form conventions where useful:
+
+- `/ai/*` menu grouping and settings submenu patterns.
+- Dense management tables with row navigation, bulk actions, and contextual menus.
+- Tabbed agent editor structure for metadata, input/output variables, service/plugin selection, and execution settings.
+- Refit-style client interfaces and FluentValidation-backed forms.
+
+Do not carry forward raw API key value display, provider-specific service JSON editing as the primary user journey, or a management-page-first experience. Weaver opens on chat and proposal review; administration remains secondary and permission-gated.
+
+## Studio Non-Responsibilities
+
+- Do not invoke model providers.
+- Do not host AI runtimes.
+- Do not send provider credentials.
+- Do not send raw workflow/runtime datasets when a reference can be sent.
+- Do not apply workflow changes client-side.
+
+## Context Attachment Shape
+
+```ts
+type AIContextAttachment =
+ | { kind: "WorkflowDefinition"; referenceId: string; activityId?: string }
+ | { kind: "WorkflowInstance"; referenceId: string; activityId?: string }
+ | { kind: "DiagnosticsScope"; referenceId?: string; timeRange?: AITimeRange }
+ | { kind: "Tenant"; referenceId: string }
+ | { kind: "TimeRange"; timeRange: AITimeRange };
+```
+
+## Stream Rendering Expectations
+
+- `assistant.delta`: append text to the active assistant message.
+- `tool.started`: show the tool name, status, and safe arguments summary.
+- `tool.progress`: update the tool progress row.
+- `tool.completed`: show result summary and optional safe details.
+- `proposal.created`: show a proposal notification and link to review.
+- `conversation.failed`: show failure state without losing completed prior events.
+
+## Proposal Review UI
+
+The review surface displays:
+
+- Proposal status.
+- Rationale.
+- Workflow payload preview.
+- Graph diff or preview.
+- Validation diagnostics.
+- Warnings.
+- Approval, rejection, and apply actions based on permissions and proposal state.
+
+## Conversation Persistence
+
+Studio can request existing conversation history from server APIs once those endpoints are implemented. Provider session details remain server-only.
diff --git a/specs/008-weaver-ai-copilot/contracts/tool-governance.md b/specs/008-weaver-ai-copilot/contracts/tool-governance.md
new file mode 100644
index 000000000..f30166178
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/contracts/tool-governance.md
@@ -0,0 +1,62 @@
+# Tool Governance Contract: Weaver AI Copilot Platform
+
+## Required Tool Metadata
+
+Every tool must declare:
+
+- `Name`: stable namespaced identifier.
+- `Description`: concise behavior description.
+- `Schema`: structured input schema.
+- `Mutability`: `ReadOnly`, `Proposal`, or `Administrative`.
+- `DangerLevel`: `Low`, `Medium`, `High`, or `Critical`.
+- `Permissions`: required Elsa permissions.
+- `TenantBehavior`: `TenantScoped`, `HostScoped`, or `CrossTenantDenied`.
+- `AuditBehavior`: `None`, `RecordInvocation`, or `RecordInvocationAndResult`.
+- `AgentScopes`: optional agent allowlist.
+
+## Registration APIs
+
+```csharp
+public static class AIFeatureExtensions
+{
+ public static AIFeature AddAITool(this AIFeature feature) where TTool : class, IAITool;
+ public static AIFeature AddAIContextProvider(this AIFeature feature) where TProvider : class, IAIContextProvider;
+ public static AIFeature AddAIAgent(this AIFeature feature) where TAgent : class, IAIAgentDefinitionProvider;
+ public static AIFeature AddMcpServer(this AIFeature feature, Action configure);
+}
+```
+
+## Enforcement Order
+
+1. Resolve current actor and tenant.
+2. Resolve agent scope.
+3. Find tool definition.
+4. Validate tool is enabled globally and for the agent.
+5. Validate tenant behavior.
+6. Validate permissions and ownership.
+7. Validate mutability and danger policy.
+8. Redact arguments.
+9. Execute tool.
+10. Redact result.
+11. Emit stream and audit events.
+
+## Mutability Policy
+
+- `ReadOnly`: may inspect authorized Elsa data but cannot create proposals or mutate state.
+- `Proposal`: may create or validate proposals but cannot directly apply them.
+- `Administrative`: reserved for future work and disabled in MVP.
+
+## Enablement Policy
+
+- Read-only tools registered by enabled modules may be enabled by default.
+- Proposal tools require explicit administrator enablement.
+- Administrative tools are disabled in MVP even if registered.
+- MCP-backed tools require explicit administrator enablement and allowlisted tool names.
+
+## MCP Policy
+
+- MCP servers are configured server-side.
+- Only allowlisted MCP tools are exposed.
+- MCP tools inherit the same metadata and enforcement path as built-in tools.
+- Per-agent MCP scoping is required.
+- MCP tools are unavailable until explicitly enabled by an administrator.
diff --git a/specs/008-weaver-ai-copilot/data-model.md b/specs/008-weaver-ai-copilot/data-model.md
new file mode 100644
index 000000000..90c5658ab
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/data-model.md
@@ -0,0 +1,116 @@
+# Data Model: Weaver AI Copilot Platform
+
+## AIConversation
+
+Represents a Weaver chat thread visible to a user.
+
+**Fields**: `Id`, `TenantId`, `UserId`, `Title`, `Status`, `CreatedAt`, `UpdatedAt`, `ProviderSessionId`, `RetentionMode`, `RetentionExpiresAt`.
+
+**Relationships**: Has many `AIMessage`, `AIContextAttachment`, `AIToolInvocation`, and `AIAuditEvent` records.
+
+**Rules**:
+
+- Conversation access is tenant- and actor-scoped.
+- Provider session IDs are internal server metadata and never exposed as provider objects.
+- Retention policy controls conversation history visibility, durability, and cleanup.
+
+## AIMessage
+
+Represents user, assistant, system, or tool-visible content in a conversation.
+
+**Fields**: `Id`, `ConversationId`, `Role`, `Content`, `CreatedAt`, `StreamSequence`, `Metadata`.
+
+**Rules**:
+
+- Stored content must already be redacted.
+- Tool result details may be summarized for model context and preserved separately for display when safe.
+
+## AIContextAttachment
+
+Represents a client-supplied reference to server-resolved context.
+
+**Fields**: `Id`, `ConversationId`, `Kind`, `ReferenceId`, `TenantId`, `Scope`, `TimeRange`, `ActivityId`, `Metadata`.
+
+**Supported Kinds**: `WorkflowDefinition`, `WorkflowInstance`, `ActivitySelection`, `Tenant`, `DiagnosticsScope`, `TimeRange`.
+
+**Rules**:
+
+- Studio sends references only.
+- Server resolves attachments per request and validates authorization before use.
+- Context resolution applies size limits and redaction.
+
+## AIToolDefinition
+
+Represents a registered Weaver tool.
+
+**Fields**: `Name`, `DisplayName`, `Description`, `Schema`, `Mutability`, `DangerLevel`, `Permissions`, `TenantBehavior`, `AuditBehavior`, `AgentScopes`, `Provider`, `EnabledByDefault`, `IsEnabled`.
+
+**Rules**:
+
+- Tool names are stable and namespaced, such as `workflow.getDefinition`.
+- Mutating tools are either proposal-only or administrative; administrative tools are out of MVP scope.
+- Tool metadata is validated during registration.
+- Read-only module tools may be enabled by default; proposal, administrative, and MCP-backed tools require explicit administrator enablement.
+
+## AIToolInvocation
+
+Represents a single attempted tool execution.
+
+**Fields**: `Id`, `ConversationId`, `ToolName`, `Arguments`, `AuthorizationResult`, `StartedAt`, `CompletedAt`, `Status`, `ResultSummary`, `Error`, `TraceId`, `TenantId`, `ActorId`.
+
+**Rules**:
+
+- Authorization is checked before execution.
+- Arguments and results are redacted before audit and stream output.
+- Failed and denied invocations are audited.
+
+## AIProposal
+
+Represents a reviewable AI-generated workflow creation or update.
+
+**Fields**: `Id`, `TenantId`, `ConversationId`, `Kind`, `Status`, `BaselineWorkflowDefinitionId`, `BaselineVersionId`, `WorkflowPayload`, `Rationale`, `Warnings`, `ValidationDiagnostics`, `GraphDiff`, `CreatedBy`, `CreatedAt`, `ReviewedBy`, `ReviewedAt`, `AppliedBy`, `AppliedAt`.
+
+**States**: `Draft`, `Validated`, `Blocked`, `Approved`, `Rejected`, `Applied`, `Expired`.
+
+**Rules**:
+
+- Proposals are the only AI path to workflow writes.
+- Apply requires approval, authorization, validation pass, and baseline match.
+- The same authorized user may create, approve, reject, and apply a proposal in MVP.
+- Proposals are durable governance artifacts.
+- Proposal payloads are structured workflow definitions or patches, not free-form text.
+
+## AIAuditEvent
+
+Represents durable governance evidence.
+
+**Fields**: `Id`, `TenantId`, `ActorId`, `ConversationId`, `ProposalId`, `ToolInvocationId`, `Type`, `Timestamp`, `TraceId`, `Summary`, `Data`.
+
+**Rules**:
+
+- Audit data is redacted.
+- Prompt, context resolution, tool call, denial, proposal, approval, rejection, and apply events are recorded.
+
+## AIAgentDefinition
+
+Represents a named Weaver agent contributed by Core or a module.
+
+**Fields**: `Name`, `DisplayName`, `Description`, `Instructions`, `AllowedTools`, `AllowedContextProviders`, `AllowedMcpServers`, `Permissions`, `Enabled`.
+
+**Rules**:
+
+- Agents operate with least-privilege tool and context scopes.
+- Agent instructions cannot grant permissions or bypass server enforcement.
+
+## AIMcpServerRegistration
+
+Represents a governed external tool server registration.
+
+**Fields**: `Name`, `Transport`, `Endpoint`, `ToolAllowlist`, `AgentScopes`, `Permissions`, `TenantBehavior`, `AuditBehavior`, `Enabled`.
+
+**Rules**:
+
+- Only allowlisted MCP tools are exposed.
+- Local and remote servers are configured server-side.
+- Per-agent scoping is mandatory.
+- MCP-backed tools require explicit administrator enablement before use.
diff --git a/specs/008-weaver-ai-copilot/plan.md b/specs/008-weaver-ai-copilot/plan.md
new file mode 100644
index 000000000..ed782a60e
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/plan.md
@@ -0,0 +1,169 @@
+# Implementation Plan: Weaver AI Copilot Platform
+
+**Branch**: `codex/008-weaver-ai-copilot` | **Date**: 2026-05-21 | **Spec**: [spec.md](./spec.md)
+**Input**: Feature specification from `/specs/008-weaver-ai-copilot/spec.md`
+
+## Summary
+
+Introduce Weaver as Elsa's AI copilot platform: a server-hosted, provider-isolated AI orchestration layer with Studio chat, governed tool execution, context providers, streaming events, durable audit records, and durable proposal-only workflow mutations. The first delivery establishes `Elsa.AI.Abstractions`, `Elsa.AI.Host`, `Elsa.AI.Copilot`, durable proposal/audit persistence, and a paired `Elsa.Studio.AI` module, with read-only workflow/runtime tools and safe workflow proposal flows.
+
+## Technical Context
+
+**Language/Version**: C# latest, nullable reference types enabled, implicit usings enabled; paired Studio Blazor/Razor module work in the Studio repository.
+**Primary Dependencies**: Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, existing identity/authorization and tenancy services, workflow definition/instance abstractions, diagnostics/log abstractions, `Microsoft.Extensions.Options`, `Microsoft.Extensions.Logging`, OpenTelemetry, `System.Text.Json`, SignalR or SSE streaming, GitHub Copilot SDK isolated behind `Elsa.AI.Copilot`, and headless Copilot CLI JSON-RPC integration.
+**Storage**: Configurable conversation/session retention with in-memory support for development and tests; durable proposal and audit stores required for MVP using Elsa persistence provider abstractions and an EF Core provider package for production.
+**Testing**: xUnit unit tests for abstractions, tool metadata, authorization gates, context redaction, proposal lifecycle, persistence state transitions, explicit tool enablement, reconnect handling, and adapter mapping; integration tests for chat streaming, tool invocation, capabilities, proposal apply, tenant isolation, durable audit records, durable proposals, and scoped trend analysis; contract tests for Studio-facing API and stream event shapes.
+**Target Platform**: ASP.NET Core Elsa Server on supported .NET target frameworks, plus Elsa Studio SPA integration through server APIs only.
+**Project Type**: Modular .NET server libraries with REST/streaming APIs and a paired Studio UI module.
+**Performance Goals**: First streamed chat event within 3 seconds p95 after server accepts a turn under normal load; tool metadata/capabilities under 250 ms p95; proposal validation under 5 seconds p95 for typical workflow definitions; bounded server-side context payloads.
+**Constraints**: Studio is AI-agnostic; AI runtime is server-hosted; provider SDK types cannot leak into core abstractions, workflow models, or Studio contracts; AI writes are proposal-only; same authorized user may request/approve/apply proposals in MVP; all tools enforce tenant/RBAC/ownership server-side; proposal/admin/MCP tools require explicit administrator enablement; runtime trend analysis is scoped to attached references plus selected time range and diagnostics scope; secrets and sensitive config are redacted before model context, stream output, and audit records; implementation execution should use dedicated git worktrees for Core and paired Studio work rather than using a local primary checkout directly.
+**Scale/Scope**: Server AI abstractions, Copilot adapter, chat/session orchestration with configurable disconnect grace, stream event translation, tool registry, context providers, durable audit sink, durable proposal store, MVP workflow/runtime tools, proposal apply endpoint, Studio chat/proposal UX contracts, and extension APIs for third-party tools/agents/MCP registrations.
+
+## Constitution Check
+
+Evaluated against `.specify/memory/constitution.md` v1.1.0:
+
+| Principle | Verdict | Evidence |
+|-----------|---------|----------|
+| I. Modular Architecture | PASS | AI abstractions, host/orchestration, provider adapter, and Studio UI are separate modules with published contracts. |
+| II. Composition & Extensibility | PASS | Tool registry, context providers, proposal store, audit sink, provider adapter, agents, and MCP registrations are explicit extension points. |
+| III. Convention-Driven Design | PASS | Endpoints use Elsa endpoint patterns; modules, features, contracts, and tests follow repository naming conventions and American English. |
+| IV. Async & Pipeline Execution | PASS | Chat, streaming, tool execution, context resolution, validation, audit, and persistence contracts are async and cancellation-aware. |
+| V. Testing Discipline | PASS | Plan includes unit, integration, and contract coverage for governance, streaming, proposal safety, and adapter boundaries. |
+| VI. Trunk-Based Development | PASS | This plan is a single feature concern; Core and Studio repository work are documented as separate implementation surfaces. |
+| VII. Simplicity, SRP, DRY & KISS | PASS | MVP defers administrative mutation tools and limits durable persistence to proposal and audit governance records; provider volatility is contained in one adapter module. |
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/008-weaver-ai-copilot/
+├── spec.md
+├── plan.md
+├── research.md
+├── data-model.md
+├── quickstart.md
+├── contracts/
+│ ├── runtime-contract.md
+│ ├── rest-api.md
+│ ├── studio-contract.md
+│ └── tool-governance.md
+├── checklists/
+│ └── requirements.md
+└── tasks.md
+```
+
+### Source Code (repository root)
+
+```text
+src/modules/
+├── Elsa.AI.Abstractions/
+│ ├── Contracts/
+│ ├── Models/
+│ ├── Options/
+│ └── Extensions/
+├── Elsa.AI.Host/
+│ ├── Context/
+│ ├── Endpoints/AI/
+│ ├── Extensions/
+│ ├── Features/
+│ ├── Models/
+│ ├── Permissions/
+│ ├── Proposals/
+│ ├── Services/
+│ ├── ShellFeatures/
+│ ├── Streaming/
+│ └── Tools/
+├── Elsa.AI.Copilot/
+│ ├── Adapters/
+│ ├── Extensions/
+│ ├── Features/
+│ ├── Models/
+│ ├── Options/
+│ ├── ShellFeatures/
+│ └── Services/
+└── Elsa.AI.Persistence.EFCore/
+ ├── Configurations/
+ ├── Entities/
+ ├── Extensions/
+ ├── Features/
+ ├── Migrations/
+ ├── ShellFeatures/
+ └── Stores/
+
+test/unit/
+├── Elsa.AI.Abstractions.UnitTests/
+├── Elsa.AI.Host.UnitTests/
+├── Elsa.AI.Copilot.UnitTests/
+└── Elsa.AI.Persistence.EFCore.UnitTests/
+
+test/integration/
+└── Elsa.AI.IntegrationTests/
+```
+
+### Paired Studio Repository
+
+```text
+src/modules/
+└── Elsa.Studio.AI/
+ ├── Client/
+ ├── Contracts/
+ ├── Extensions/
+ ├── Feature.cs
+ ├── Services/
+ ├── UI/
+ │ ├── Components/
+ │ └── Pages/
+ └── _Imports.razor
+```
+
+**UI Prototype Reference**: Review `elsa-extensions` branch `origin/feat/ai` at commit `93f0e09d71e57f5daff1e2d593f0a51faaa80417` and its parent chain before implementing Studio UI. Useful patterns include the Razor/MudBlazor Agents menu placement under `/ai/*`, management tables, route structure, Refit client interfaces, validators, and agent configuration tabs for general metadata, input/output variables, services, plugins, and execution settings. Do not carry forward raw API key reveal, provider-specific service configuration as the primary experience, or an agent-management-first flow; Weaver's first screen remains the chat/proposal experience.
+
+**Structure Decision**: Keep provider-neutral contracts in `Elsa.AI.Abstractions`, server orchestration, APIs, built-in tools, proposals, and audit in `Elsa.AI.Host`, and Copilot SDK/CLI integration in `Elsa.AI.Copilot`. The Studio module consumes only REST and streaming contracts; if the Studio repository is not present, its implementation tasks become a sibling-repository follow-up.
+
+## Phase 0 Output
+
+See [research.md](./research.md).
+
+Resolved decisions:
+
+- Use a server-hosted AI runtime with Studio sending only references.
+- Isolate GitHub Copilot SDK and headless CLI JSON-RPC behind `Elsa.AI.Copilot`.
+- Translate provider stream events into Elsa-owned stream contracts.
+- Use proposal-only writes for workflow creation and updates.
+- Use configurable conversation retention, but require durable proposal and audit stores for MVP.
+- Allow the same authorized user to request, approve, reject, and apply proposals in MVP, with explicit actions and durable audit records.
+- Require explicit administrator enablement for proposal, administrative, and MCP-backed tools; read-only module tools may be available by default.
+- Continue disconnected chat turns for a configurable grace window and allow authorized clients to reconnect to durable outputs.
+- Limit runtime trend analysis by default to attached references plus explicit time range and diagnostics scope.
+- Treat MCP and custom agents as governed extension registrations, not unrestricted runtime configuration.
+
+## Phase 1 Output
+
+- [data-model.md](./data-model.md)
+- [contracts/runtime-contract.md](./contracts/runtime-contract.md)
+- [contracts/rest-api.md](./contracts/rest-api.md)
+- [contracts/studio-contract.md](./contracts/studio-contract.md)
+- [contracts/tool-governance.md](./contracts/tool-governance.md)
+- [quickstart.md](./quickstart.md)
+
+## Post-Design Constitution Re-Check
+
+| Principle | Verdict | Post-design evidence |
+|-----------|---------|----------------------|
+| I. Modular Architecture | PASS | Contracts separate abstractions, host behavior, provider adapter, and Studio-facing UI contracts. |
+| II. Composition & Extensibility | PASS | Tool, context, agent, provider, MCP, proposal, audit, and streaming contracts are replaceable. |
+| III. Convention-Driven Design | PASS | Contract and API names align with Elsa module and endpoint conventions. |
+| IV. Async & Pipeline Execution | PASS | Design contracts use async APIs and stream abstractions for all I/O and tool work. |
+| V. Testing Discipline | PASS | Quickstart and tasks define unit, integration, and contract verification before implementation. |
+| VI. Trunk-Based Development | PASS | Implementation can ship incrementally by independently testable user story phases. |
+| VII. Simplicity, SRP, DRY & KISS | PASS | MVP excludes administrative tools, generic agent runtime reimplementation, direct Studio/provider coupling, and durable conversation persistence as a requirement. |
+
+## Phase 2 Handoff
+
+[tasks.md](./tasks.md) has been generated and analysis-remediated to reflect durable proposal/audit persistence, explicit tool enablement, reconnect handling, scoped trend analysis, ShellFeature registration, provider/BYOK configuration, OpenTelemetry, redaction boundaries, retention tests, and worktree-based execution. Suggested MVP cut is Phase 1 setup, Phase 2 foundation, durable proposal/audit storage, and User Story 1 chat/tool streaming. Proposal generation is the next reviewable increment.
+
+## Complexity Tracking
+
+No constitution violations identified.
diff --git a/specs/008-weaver-ai-copilot/quickstart.md b/specs/008-weaver-ai-copilot/quickstart.md
new file mode 100644
index 000000000..7e427d91a
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/quickstart.md
@@ -0,0 +1,87 @@
+# Quickstart: Weaver AI Copilot Platform
+
+## Goal
+
+Validate the MVP path for Weaver without relying on direct Studio-to-provider calls:
+
+1. Enable AI abstractions, host, and Copilot adapter modules.
+2. Start a server-hosted Weaver chat.
+3. Resolve workflow context server-side.
+4. Stream assistant and tool events.
+5. Create a workflow proposal.
+6. Validate, approve, apply, and durably audit the proposal.
+
+## Worktree Setup
+
+Implementation should run from dedicated git worktrees, not the primary local checkout. Create separate worktrees for Core and the paired Studio module so feature work, generated artifacts, and test output stay isolated:
+
+```bash
+git -C /Users/sipke/Projects/Elsa/elsa-core worktree add -b codex/008-weaver-ai-copilot-core ../elsa-core-weaver codex/008-weaver-ai-copilot
+git -C /Users/sipke/Projects/Elsa/elsa-studio worktree add -b codex/008-weaver-ai-copilot-studio ../elsa-studio-weaver main
+```
+
+Run Core tasks from `/Users/sipke/Projects/Elsa/elsa-core-weaver` and Studio tasks from `/Users/sipke/Projects/Elsa/elsa-studio-weaver`. If either target branch already exists, attach the worktree to that branch instead of creating a duplicate branch.
+
+## Server Setup
+
+```csharp
+services
+ .AddElsa(elsa =>
+ {
+ elsa.UseAI(ai =>
+ {
+ ai.UseHost();
+ ai.UseCopilot(copilot =>
+ {
+ copilot.CliPath = "copilot";
+ copilot.Model = "configured-model";
+ });
+ });
+ });
+```
+
+## Manual Validation
+
+1. Start Elsa Server with Weaver enabled.
+2. Request `GET /ai/capabilities` and verify streaming, proposal review, attachment kinds, and agents are listed.
+3. Request `GET /ai/tools` as an authorized user and verify MVP tools are returned.
+4. Start `POST /ai/chat` with a `WorkflowDefinition` attachment reference and ask Weaver to explain it.
+5. Verify stream events include assistant deltas and any tool lifecycle events.
+6. Ask Weaver to generate a simple workflow.
+7. Verify a `proposal.created` event appears and `GET /ai/proposals/{id}` returns payload, rationale, warnings, diagnostics, and graph preview.
+8. Attempt to apply without approval and verify the server rejects the transition.
+9. Approve and apply the proposal as an authorized user.
+10. Verify the workflow is persisted, validation passed, and durable audit records exist for prompt, tool calls, approval, and apply.
+11. Restart the server with durable persistence configured and verify proposals and audit records are still available.
+12. Disconnect during a chat turn, reconnect within the configured grace window, and verify durable outputs produced while disconnected are recoverable.
+13. Ask for runtime trends using attached references plus a selected time range and diagnostics scope, then verify results do not include data outside that scope.
+
+## Targeted Test Commands
+
+```bash
+dotnet test test/unit/Elsa.AI.Abstractions.UnitTests/Elsa.AI.Abstractions.UnitTests.csproj
+dotnet test test/unit/Elsa.AI.Host.UnitTests/Elsa.AI.Host.UnitTests.csproj
+dotnet test test/unit/Elsa.AI.Copilot.UnitTests/Elsa.AI.Copilot.UnitTests.csproj
+dotnet test test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj
+```
+
+## Boundary Checks
+
+```bash
+rg "Copilot" src/modules/Elsa.AI.Abstractions src/modules/Elsa.AI.Host
+rg "GitHub" src/modules/Elsa.AI.Abstractions src/modules/Elsa.AI.Host
+```
+
+Expected result: no provider SDK types or provider-owned event names appear in abstractions, host contracts, workflow models, or Studio contracts.
+
+## Studio Prototype Review
+
+Before implementing the paired Studio module, inspect the prototype branch:
+
+```bash
+git -C ../elsa-extensions-investigation fetch origin feat/ai:refs/remotes/origin/feat/ai
+git -C ../elsa-extensions-investigation show --stat 93f0e09d71e57f5daff1e2d593f0a51faaa80417
+git -C ../elsa-extensions-investigation diff --name-status main..origin/feat/ai -- src/modules/agents/Elsa.Studio.Agents
+```
+
+Use it for route, menu, table, dialog, validation, and agent editor conventions. Keep Weaver chat and proposal review as the primary UI.
diff --git a/specs/008-weaver-ai-copilot/research.md b/specs/008-weaver-ai-copilot/research.md
new file mode 100644
index 000000000..f5b7ab695
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/research.md
@@ -0,0 +1,123 @@
+# Research: Weaver AI Copilot Platform
+
+## Decision: Studio remains AI-agnostic
+
+**Rationale**: Studio should render chat, stream events, tool progress, and proposals, but it must not host model runtimes, carry provider credentials, or send raw workflow/runtime data to providers. Server-side context resolution preserves tenancy, RBAC, redaction, and audit.
+
+**Alternatives considered**:
+
+- Direct Studio-to-provider calls: rejected because it leaks provider details, credentials, and raw data into the SPA.
+- Embedding provider SDKs in Studio: rejected because it couples UI releases to volatile AI runtime APIs.
+
+## Decision: Elsa Server hosts Weaver orchestration
+
+**Rationale**: Server hosting lets Elsa enforce tenant scope, permissions, proposal lifecycle, validation, redaction, audit, and telemetry before tools or models see data.
+
+**Alternatives considered**:
+
+- External standalone agent service: deferred because it adds deployment and authorization complexity before the Elsa-owned governance model is proven.
+- Reimplementing a generic agent runtime: rejected because the product goal is Elsa-specific governance and workflow intelligence, not a new general-purpose runtime.
+
+## Decision: Isolate Copilot integration behind `Elsa.AI.Copilot`
+
+**Rationale**: GitHub documents the Copilot SDK as public preview, with functionality subject to change. The adapter boundary protects Elsa abstractions, Studio contracts, and workflow models from provider SDK churn while still using Copilot runtime features.
+
+**Sources**: [GitHub Copilot SDK custom agents](https://docs.github.com/en/copilot/how-tos/copilot-sdk/use-copilot-sdk/custom-agents), [SDK and CLI compatibility](https://docs.github.com/en/copilot/how-tos/copilot-sdk/troubleshooting/sdk-and-cli-compatibility)
+
+**Alternatives considered**:
+
+- Expose Copilot SDK types in server contracts: rejected because it would make Elsa public APIs depend on preview runtime details.
+- Build provider-specific behavior into `Elsa.AI.Host`: rejected because provider isolation is a core architectural requirement.
+
+## Decision: Translate provider events into Elsa stream events
+
+**Rationale**: Copilot SDK emits assistant deltas, tool execution lifecycle events, permission/user-input events, session lifecycle events, and sub-agent events. Elsa should map those into stable `AIStreamEvent` contracts for Studio.
+
+**Sources**: [Streaming events in the Copilot SDK](https://docs.github.com/en/enterprise-cloud%40latest/copilot/how-tos/copilot-sdk/use-copilot-sdk/streaming-events)
+
+**Alternatives considered**:
+
+- Pass provider event envelopes directly to Studio: rejected because it leaks provider-specific event names and fields.
+- Buffer full responses only: rejected because the PRD requires streaming deltas and tool progress.
+
+## Decision: Use proposal-only workflow mutations
+
+**Rationale**: Workflow creation and edits must be reviewable, validated, auditable, and applied only after user approval. This keeps AI from directly mutating production artifacts and lets Elsa own workflow semantics.
+
+**Alternatives considered**:
+
+- AI tools save definitions directly after validation: rejected because validation does not replace user review and approval.
+- Client-side proposal apply: rejected because apply must enforce server authorization, stale baseline checks, validation, and audit.
+
+## Decision: Built-in tools use Elsa abstractions only
+
+**Rationale**: Tools must respect existing workflow stores, instance stores, activity catalog services, diagnostics sources, authorization, and tenancy. Direct persistence access would bypass existing boundaries and make provider tools harder to govern.
+
+**Alternatives considered**:
+
+- Tool-specific repository access: rejected because it duplicates existing service contracts and weakens authorization boundaries.
+- Prompt-only context injection: rejected because large datasets should be queried through scoped tools rather than dumped into prompts.
+
+## Decision: Require durable proposal and audit storage, with configurable conversation retention
+
+**Rationale**: Workflow proposals and audit records are governance artifacts, so MVP must persist them durably. Conversation and session history can remain retention-configurable because chat transcript durability is less critical than proposal review, approval, application, and audit evidence.
+
+**Alternatives considered**:
+
+- In-memory proposals and audit for MVP: rejected because proposal review and audit evidence must survive server restarts.
+- Durable conversations, proposals, and audit in MVP: partially rejected because conversation history can be configurable without weakening workflow mutation governance.
+- Provider-owned conversation persistence only: rejected because Elsa must audit and govern conversations independently of provider internals.
+
+## Decision: Govern MCP and custom agents as extension registrations
+
+**Rationale**: Copilot supports custom agents with scoped tools and MCP servers. Elsa should expose module registration APIs that declare scope, permissions, mutability, danger level, and audit policy before any external tool becomes available.
+
+**Sources**: [GitHub Copilot SDK custom agents](https://docs.github.com/en/copilot/how-tos/copilot-sdk/use-copilot-sdk/custom-agents), [GitHub Copilot CLI command reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference)
+
+**Alternatives considered**:
+
+- Let operators pass arbitrary MCP configuration from Studio: rejected because it bypasses governance.
+- Make all tools available to every agent: rejected because least-privilege tool scoping is required for safe delegation.
+
+## Decision: Enable tools by mutability and integration risk
+
+**Rationale**: Read-only module tools are low-risk when existing module permissions and tenant checks apply. Proposal, administrative, and MCP-backed tools can affect workflow changes or external integrations, so they require explicit administrator enablement before use.
+
+**Alternatives considered**:
+
+- Auto-enable every registered tool: rejected because MCP and proposal tools need administrator review.
+- Require manual enablement for every read-only module tool: rejected because it adds friction for low-risk module capabilities already protected by permissions.
+
+## Decision: Continue disconnected chat turns for a grace window
+
+**Rationale**: A transient browser disconnect should not immediately abandon a useful AI turn, but the server should not run expensive provider work indefinitely without an attached client. A configurable grace window balances user experience and resource control.
+
+**Alternatives considered**:
+
+- Cancel immediately on disconnect: rejected because it makes streaming fragile.
+- Always run to completion after disconnect: rejected because it can waste provider and server resources.
+
+## Decision: Scope runtime trend analysis to explicit user-selected context
+
+**Rationale**: Trend analysis is useful only when broad enough to inspect multiple failures, but unrestricted tenant-wide analysis risks unexpected data exposure and large prompt/tool workloads. Attached references plus explicit time range and diagnostics scope keep access intentional and auditable.
+
+**Alternatives considered**:
+
+- Only inspect attached instances: rejected because trend analysis needs a bounded set of related runtime data.
+- Inspect any readable tenant runtime data: rejected because it is too broad by default.
+
+## Decision: Use the `elsa-extensions` AI prototype as UI precedent, not as final UX
+
+**Rationale**: Branch `origin/feat/ai` at commit `93f0e09d71e57f5daff1e2d593f0a51faaa80417` contains useful Studio patterns for `/ai/*` navigation, Agents menu placement, dense management tables, API clients, validators, and agent editor tabs. Weaver should reuse those interaction patterns where they fit Elsa Studio conventions, but the primary UX must be chat and proposal review rather than CRUD-first agent/provider administration.
+
+**Prototype elements to consider**:
+
+- `src/modules/agents/Elsa.Studio.Agents/AgentsMenu.cs` for `/ai/agents`, `/ai/api-keys`, and `/ai/services` menu organization.
+- `UI/Pages/Agents.razor` for dense table, bulk actions, row navigation, and create action patterns.
+- `UI/Pages/Agent.razor` for tabbed agent configuration: General, Input, Output, Services, Plugins, and Execution Settings.
+- `Client/IAgentsApi.cs`, `IApiKeysApi.cs`, `IServicesApi.cs`, and validators for Studio API client and form validation structure.
+
+**Alternatives considered**:
+
+- Copy the prototype UI as-is: rejected because Weaver's MVP is chat/proposal review, and the prototype exposes API key values and provider-specific service settings too directly.
+- Ignore the prototype: rejected because it captures useful Elsa Studio route, menu, table, dialog, and editor conventions for AI-related surfaces.
diff --git a/specs/008-weaver-ai-copilot/spec.md b/specs/008-weaver-ai-copilot/spec.md
new file mode 100644
index 000000000..dbfd8f62b
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/spec.md
@@ -0,0 +1,177 @@
+# Feature Specification: Weaver AI Copilot Platform
+
+**Feature Branch**: `codex/008-weaver-ai-copilot`
+**Created**: 2026-05-20
+**Status**: Draft
+**Input**: User description: "Implement Weaver, an AI assistant / agent that provides agentic workflow authoring and management similar to GitHub Copilot, integrated as a chat interface in Elsa Studio."
+
+## Clarifications
+
+### Session 2026-05-20
+
+- Q: What durability level should Weaver require for MVP conversations, proposals, and audit records? → A: Durable proposals and audit required; conversation history retention configurable.
+- Q: Should Weaver require separation of duties between the user who requests an AI proposal and the user who approves/applies it? → A: Same authorized user may request, approve, and apply proposals.
+- Q: When third-party modules or MCP servers contribute Weaver tools, should those tools become available automatically? → A: Auto-enable read-only module tools; require explicit enablement for proposal, administrative, and MCP tools.
+
+### Session 2026-05-21
+
+- Q: What should Weaver do when chat stream clients disconnect during an in-progress turn? → A: Continue the turn for a short grace window, persist durable outputs, and allow reconnect.
+- Q: For runtime trend analysis, what data scope may Weaver inspect by default? → A: Attached references plus user-selected time range and diagnostics scope.
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Chat with workflow-aware Weaver (Priority: P1)
+
+An Elsa Studio user opens a Weaver panel, attaches workflow or runtime references, asks a question, and receives a streaming answer with visible tool progress and results.
+
+**Why this priority**: Conversational access is the foundation for every authoring, diagnostics, and operations use case.
+
+**Independent Test**: Start a chat with a workflow definition reference, verify the server resolves context, streams message deltas, emits tool progress, and completes the conversation without Studio contacting an AI provider directly.
+
+**Acceptance Scenarios**:
+
+1. **Given** a user with permission to view a workflow definition, **When** they ask Weaver to explain that workflow, **Then** Weaver streams an answer grounded in server-resolved workflow context.
+2. **Given** a chat request with context attachment references, **When** the server receives the request, **Then** the server resolves only the authorized data needed for the turn and redacts sensitive values before model context is built.
+3. **Given** a tool is invoked during a chat turn, **When** the tool starts, progresses, returns, or fails, **Then** Studio receives stream events that can render the tool lifecycle.
+
+---
+
+### User Story 2 - Generate workflows through reviewable proposals (Priority: P2)
+
+A workflow author describes a desired workflow in natural language and receives a structured workflow proposal with rationale, validation diagnostics, warnings, and graph diff before any workflow is persisted.
+
+**Why this priority**: Safe creation is the first high-value authoring workflow and proves the proposal-only mutation model.
+
+**Independent Test**: Ask Weaver to create a workflow, inspect the proposal, validate it, approve it, and verify the workflow is applied only after approval and audit entries are recorded.
+
+**Acceptance Scenarios**:
+
+1. **Given** a user can create workflows, **When** they ask Weaver to generate one, **Then** Weaver creates a proposal with workflow payload, rationale, warnings, validation diagnostics, graph preview, and proposal ID.
+2. **Given** a generated proposal has validation errors, **When** the user attempts to apply it, **Then** the server blocks application and returns the validation diagnostics.
+3. **Given** a valid proposal is approved, **When** the user applies it, **Then** the server persists the workflow and records the approval, actor, tenant, and applied change.
+
+---
+
+### User Story 3 - Edit and validate existing workflows safely (Priority: P3)
+
+A workflow author asks Weaver to modify, explain, or validate an existing workflow and receives proposed changes with an understandable graph diff and risk summary.
+
+**Why this priority**: Editing existing workflows is more common and riskier than greenfield creation, so it builds on the proven proposal lifecycle.
+
+**Independent Test**: Attach an existing workflow, ask Weaver for a targeted change, verify the returned diff is reviewable, validation runs before apply, and direct AI persistence is impossible.
+
+**Acceptance Scenarios**:
+
+1. **Given** a user can edit a workflow, **When** they ask Weaver to add or change workflow behavior, **Then** Weaver returns a proposal rather than directly saving changes.
+2. **Given** a proposed edit may break existing behavior, **When** validation runs, **Then** the proposal includes warnings and blocking diagnostics that Studio can display.
+3. **Given** a user asks why a workflow behaves a certain way, **When** Weaver analyzes the definition, **Then** the answer explains structure, activities, inputs, outputs, and likely execution path without creating a proposal.
+
+---
+
+### User Story 4 - Analyze runtime incidents conversationally (Priority: P4)
+
+An operator attaches workflow instance, diagnostics, log, or time-range references and asks Weaver to summarize failures, identify failing activities, inspect state, and highlight recurring trends.
+
+**Why this priority**: Runtime intelligence reduces operational friction and turns existing Elsa telemetry into actionable guidance.
+
+**Independent Test**: Provide failed instance and log references, ask for an incident summary, and verify Weaver uses read-only tools to produce a grounded summary with failing activities, evidence, and next investigation steps.
+
+**Acceptance Scenarios**:
+
+1. **Given** a user can inspect a workflow instance, **When** they ask Weaver why it failed, **Then** Weaver summarizes errors, activity state, relevant history, and probable causes using authorized runtime data.
+2. **Given** attached runtime references plus a user-selected time range and diagnostics scope with multiple failures, **When** the user asks for trends, **Then** Weaver identifies recurring workflows, activities, error categories, and time patterns within that selected scope.
+3. **Given** runtime data includes secrets or sensitive configuration, **When** Weaver builds context or returns results, **Then** sensitive values are redacted.
+
+---
+
+### User Story 5 - Extend Weaver with governed tools and agents (Priority: P5)
+
+A module author contributes tools, context providers, custom agents, or external tool integrations that are scoped by permissions, tenant behavior, danger level, mutability, and audit policy.
+
+**Why this priority**: Weaver must become a platform capability rather than a closed feature owned only by the core AI module.
+
+**Independent Test**: Register a module-provided read-only tool and a proposal tool, verify they appear in capabilities, execute only when authorized, and emit audit and telemetry records.
+
+**Acceptance Scenarios**:
+
+1. **Given** a module registers an AI tool, **When** Weaver capabilities are requested, **Then** the tool is listed with schema, permissions, mutability, danger level, tenant behavior, and audit behavior.
+2. **Given** a tool requires permissions the current user lacks, **When** an agent attempts to invoke it, **Then** the server denies execution and records the denial.
+3. **Given** a custom agent is registered with a scoped tool set, **When** Weaver delegates to that agent, **Then** the agent can only access its allowed tools and context providers.
+4. **Given** a module registers proposal, administrative, or MCP-backed tools, **When** the module is enabled, **Then** those tools remain unavailable until explicitly enabled by an authorized administrator.
+
+### Edge Cases
+
+- Chat requests reference deleted, inaccessible, or cross-tenant workflow data.
+- Streaming clients that disconnect mid-turn can reconnect during a configurable grace window and recover durable outputs produced while disconnected.
+- Tool execution exceeds limits, fails validation, or returns more data than can be safely included in model context.
+- Proposal payloads become stale because the workflow changed after the proposal was created.
+- Same-actor proposal approval and application still require explicit user action and audit records.
+- A third-party tool declares unsafe metadata or attempts to bypass server authorization.
+- Provider runtime is unavailable, misconfigured, or incompatible with the configured adapter version.
+- Users ask Weaver to reveal secrets, credentials, or sensitive configuration.
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: System MUST provide a Weaver chat experience in Elsa Studio that communicates only with Elsa Server APIs.
+- **FR-002**: Studio MUST send context references, not raw workflow, runtime, diagnostic, tenant, or activity data.
+- **FR-003**: Server MUST resolve context attachments according to tenant scope, ownership, and user permissions.
+- **FR-004**: System MUST stream assistant message deltas, tool progress, tool results, proposal notifications, errors, and completion events.
+- **FR-005**: System MUST support configurable conversation and session history retention; conversation durability is configurable for MVP.
+- **FR-006**: System MUST expose capabilities and tool metadata so Studio can render available Weaver behavior.
+- **FR-007**: AI-originated workflow creation and edit output MUST be represented as proposals, never direct persisted workflow mutations.
+- **FR-008**: Workflow proposals MUST include structured workflow payload, rationale, warnings, validation diagnostics, graph diff or preview, and proposal ID.
+- **FR-009**: Server MUST validate proposals before they can be applied.
+- **FR-010**: The same authorized user MUST be able to request, approve, reject, and apply proposals in MVP.
+- **FR-011**: System MUST block proposal application when validation fails or when the source workflow revision no longer matches the proposal baseline.
+- **FR-012**: System MUST durably audit prompts, model/session events, tool calls, proposal diffs, approvals, rejections, applied changes, timestamps, actors, and tenant IDs.
+- **FR-013**: System MUST provide built-in read-only workflow tools for getting definitions, listing definitions, getting instances, searching instances, and reading the activity catalog.
+- **FR-014**: System MUST provide built-in proposal tools for proposing workflow creation, proposing workflow updates, and validating draft workflow payloads.
+- **FR-015**: Runtime diagnostics tools MUST support failure inspection, incident summaries, failing activity identification, execution history review, and trend analysis.
+- **FR-016**: Tools MUST execute server-side and use published Elsa abstractions rather than bypassing authorization or direct persistence boundaries.
+- **FR-017**: Third-party modules MUST be able to register AI tools, context providers, custom agents, and external tool integrations.
+- **FR-018**: Every registered tool MUST declare schema, danger level, mutability, permissions, tenant behavior, and audit behavior.
+- **FR-019**: System MUST support local and remote external tool servers with allowlists, per-agent scoping, and explicit administrator enablement.
+- **FR-020**: System MUST keep AI provider/runtime details isolated from Studio contracts, workflow models, and Elsa core abstractions.
+- **FR-021**: System MUST support provider-agnostic configuration with a bring-your-own-key model where applicable.
+- **FR-022**: System MUST support telemetry for trace correlation, tool execution metrics, model/session events, and proposal lifecycle metrics.
+- **FR-023**: System MUST redact secrets and sensitive configuration before data enters model context, tool results, streamed events, audit records, or proposal rationale.
+- **FR-024**: Administrative mutation tools such as tenant modification, workflow deletion, or instance termination MUST remain out of MVP scope.
+- **FR-025**: Read-only tools registered by enabled modules MAY be available by default, but proposal, administrative, and MCP-backed tools MUST require explicit administrator enablement before use.
+- **FR-026**: System MUST continue in-progress chat turns for a configurable disconnect grace window, persist durable outputs produced during that window, and allow authorized clients to reconnect to recover them.
+- **FR-027**: Runtime trend analysis MUST be limited by default to attached references plus an explicit user-selected time range and diagnostics scope.
+
+### Key Entities *(include if feature involves data)*
+
+- **AI Conversation**: A user-visible Weaver chat thread with messages, participants, tenant, timestamps, context attachment references, provider session reference, and configurable retention behavior.
+- **AI Session**: Server-managed runtime session used to execute agent turns, stream events, invoke tools, and resume conversations.
+- **Context Attachment**: A reference to workflow, runtime, activity, tenant, diagnostic, or time-range context that the server resolves safely.
+- **AI Tool Definition**: Registered capability with schema, permissions, mutability, danger level, tenant behavior, audit behavior, and agent scope.
+- **Tool Invocation**: A single attempted tool execution with arguments, authorization result, progress, output summary, telemetry, and audit metadata.
+- **Workflow Proposal**: A durable reviewable AI-generated workflow creation or edit package with baseline, payload, rationale, warnings, diagnostics, graph diff, state, and apply result.
+- **AI Audit Event**: Durable record of prompt, context resolution, tool invocation, proposal lifecycle, approval, application, or denial.
+- **AI Agent Definition**: Named behavior profile with prompt, allowed tools, allowed context providers, external integrations, and governance metadata.
+- **External Tool Server Registration**: Governed local or remote integration endpoint with allowlist, agent scoping, and audit policy.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: An authorized user can start a Weaver chat with workflow context and receive first streamed output within 3 seconds under normal server load.
+- **SC-002**: At least 95% of valid workflow generation requests in MVP produce a reviewable proposal with payload, rationale, diagnostics, and graph preview without directly persisting changes.
+- **SC-003**: Proposal application is blocked 100% of the time when validation fails, authorization fails, or the workflow baseline is stale.
+- **SC-004**: Runtime incident analysis can identify the failed workflow instance, failing activity, primary error, and relevant execution evidence for seeded test incidents.
+- **SC-005**: Third-party modules can register at least one read-only tool and one proposal tool without changing core Weaver orchestration code.
+- **SC-006**: Studio never sends raw workflow/runtime datasets or provider credentials to an AI provider directly.
+- **SC-007**: All tool executions and proposal lifecycle transitions create audit records with actor and tenant metadata.
+- **SC-008**: Provider-specific runtime types are absent from Elsa core abstractions, Studio contracts, and workflow definition models.
+
+## Assumptions
+
+- Weaver is the product name for the Elsa Studio AI assistant and server-hosted AI platform surface.
+- The first implementation targets Elsa Server plus a paired Elsa Studio module; code changes in the Studio repository will be tracked separately if that repository is not present in this workspace.
+- Existing Elsa identity, tenancy, workflow definition, workflow instance, activity catalog, diagnostics, logging, and validation services remain the source of truth.
+- MVP supports workflow authoring proposals and read-only runtime diagnostics; administrative destructive actions are deferred.
+- Conversation history retention is configurable for MVP, while workflow proposals and audit records require durable persistence.
+- Provider runtime integration is isolated behind a module boundary so preview or unstable provider APIs can change without breaking Elsa contracts.
diff --git a/specs/008-weaver-ai-copilot/tasks.md b/specs/008-weaver-ai-copilot/tasks.md
new file mode 100644
index 000000000..50cf5f24e
--- /dev/null
+++ b/specs/008-weaver-ai-copilot/tasks.md
@@ -0,0 +1,355 @@
+# Tasks: Weaver AI Copilot Platform
+
+**Input**: Design documents from `/specs/008-weaver-ai-copilot/`
+**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md), [data-model.md](./data-model.md), [contracts/](./contracts), [quickstart.md](./quickstart.md)
+
+**Tests**: Included because the specification requires validation coverage for streaming, tenant-aware authorization, context redaction, durable proposal/audit persistence, proposal safety, explicit tool enablement, reconnect behavior, scoped trend analysis, tool governance, and provider isolation.
+
+**Organization**: Tasks are grouped by user story so each increment can be implemented and tested independently.
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: Can run in parallel with other marked tasks after prerequisites are satisfied.
+- **[Story]**: User story label from [spec.md](./spec.md).
+- Every task includes the primary file path to edit or create.
+
+## Phase 1: Setup
+
+**Purpose**: Create module and test project skeletons.
+
+- [X] T001 [P] Create AI abstractions project in `src/modules/Elsa.AI.Abstractions/Elsa.AI.Abstractions.csproj`.
+- [X] T002 [P] Create AI host project in `src/modules/Elsa.AI.Host/Elsa.AI.Host.csproj`.
+- [X] T003 [P] Create Copilot adapter project in `src/modules/Elsa.AI.Copilot/Elsa.AI.Copilot.csproj`.
+- [X] T004 [P] Create EF Core persistence project in `src/modules/Elsa.AI.Persistence.EFCore/Elsa.AI.Persistence.EFCore.csproj`.
+- [X] T005 [P] Create abstractions unit test project in `test/unit/Elsa.AI.Abstractions.UnitTests/Elsa.AI.Abstractions.UnitTests.csproj`.
+- [X] T006 [P] Create host unit test project in `test/unit/Elsa.AI.Host.UnitTests/Elsa.AI.Host.UnitTests.csproj`.
+- [X] T007 [P] Create Copilot unit test project in `test/unit/Elsa.AI.Copilot.UnitTests/Elsa.AI.Copilot.UnitTests.csproj`.
+- [X] T008 [P] Create EF Core persistence unit test project in `test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj`.
+- [X] T009 [P] Create AI integration test project in `test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj`.
+- [X] T010 Add AI projects and tests to `Elsa.sln`.
+- [X] T011 [P] Add abstractions global usings in `src/modules/Elsa.AI.Abstractions/Usings.cs`.
+- [X] T012 [P] Add host global usings in `src/modules/Elsa.AI.Host/Usings.cs`.
+- [X] T013 [P] Add Copilot global usings in `src/modules/Elsa.AI.Copilot/Usings.cs`.
+- [X] T014 [P] Add EF Core persistence global usings in `src/modules/Elsa.AI.Persistence.EFCore/Usings.cs`.
+- [X] T015 [P] Add unit test global usings in `test/unit/Elsa.AI.Host.UnitTests/Usings.cs`.
+- [X] T016 [P] Add integration test global usings in `test/integration/Elsa.AI.IntegrationTests/Usings.cs`.
+
+---
+
+## Phase 2: Foundational
+
+**Purpose**: Add shared contracts, models, options, registration, durable governance storage, and enablement surfaces that block user stories.
+
+**Critical**: No user story work should begin until these shared contracts and registration surfaces exist.
+
+- [X] T017 [P] Add conversation, session, message, and stream event models in `src/modules/Elsa.AI.Abstractions/Models/AIConversation.cs`.
+- [X] T018 [P] Add context attachment and resolved context models in `src/modules/Elsa.AI.Abstractions/Models/AIContextAttachment.cs`.
+- [X] T019 [P] Add tool definition, invocation, result, and enablement models in `src/modules/Elsa.AI.Abstractions/Models/AIToolDefinition.cs`.
+- [X] T020 [P] Add proposal models with durable lifecycle fields in `src/modules/Elsa.AI.Abstractions/Models/AIProposal.cs`.
+- [X] T021 [P] Add durable audit event models in `src/modules/Elsa.AI.Abstractions/Models/AIAuditEvent.cs`.
+- [X] T022 [P] Add provider and orchestrator contracts in `src/modules/Elsa.AI.Abstractions/Contracts/IAIProvider.cs`.
+- [X] T023 [P] Add tool registry and tool contracts in `src/modules/Elsa.AI.Abstractions/Contracts/IAITool.cs`.
+- [X] T024 [P] Add context provider contract in `src/modules/Elsa.AI.Abstractions/Contracts/IAIContextProvider.cs`.
+- [X] T025 [P] Add conversation store, proposal store, and audit sink contracts in `src/modules/Elsa.AI.Abstractions/Contracts/IAIProposalStore.cs`.
+- [X] T026 [P] Add AI host and provider options for retention, reconnect grace, result limits, and BYOK provider references in `src/modules/Elsa.AI.Host/Options/AIHostOptions.cs`.
+- [X] T027 [P] Add AI permissions in `src/modules/Elsa.AI.Host/Permissions/AIPermissions.cs`.
+- [X] T028 Add service registration extension in `src/modules/Elsa.AI.Host/Extensions/ServiceCollectionExtensions.cs`.
+- [X] T029 Add module feature registration in `src/modules/Elsa.AI.Host/Features/AIFeature.cs`.
+- [X] T030 Add shell feature registration in `src/modules/Elsa.AI.Host/ShellFeatures/AIFeature.cs`.
+- [X] T031 [P] Implement configurable in-memory conversation store in `src/modules/Elsa.AI.Host/Services/InMemoryAIConversationStore.cs`.
+- [X] T032 [P] Implement tool enablement service in `src/modules/Elsa.AI.Host/Services/AIToolEnablementService.cs`.
+- [X] T033 [P] Implement audit sink fan-out in `src/modules/Elsa.AI.Host/Services/AIAuditSink.cs`.
+- [X] T034 [P] Add EF Core AI DbContext in `src/modules/Elsa.AI.Persistence.EFCore/AIDbContext.cs`.
+- [X] T035 [P] Add EF Core proposal entity mapping in `src/modules/Elsa.AI.Persistence.EFCore/Entities/AIProposalRecord.cs`.
+- [X] T036 [P] Add EF Core audit entity mapping in `src/modules/Elsa.AI.Persistence.EFCore/Entities/AIAuditRecord.cs`.
+- [X] T037 Implement durable EF Core proposal store in `src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIProposalStore.cs`.
+- [X] T038 Implement durable EF Core audit sink in `src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIAuditSink.cs`.
+- [X] T039 Add EF Core persistence feature and shell feature registration in `src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/AIPersistenceFeature.cs`.
+- [X] T040 [P] Add registration and naming tests in `test/unit/Elsa.AI.Host.UnitTests/AIRegistrationTests.cs`.
+- [X] T041 [P] Add tool metadata and enablement validation tests in `test/unit/Elsa.AI.Abstractions.UnitTests/AIToolDefinitionTests.cs`.
+- [X] T042 [P] Add durable proposal persistence tests in `test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIProposalStoreTests.cs`.
+- [X] T043 [P] Add durable audit persistence tests in `test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIAuditSinkTests.cs`.
+- [X] T044 [P] Add provider isolation boundary tests in `test/unit/Elsa.AI.Copilot.UnitTests/CopilotBoundaryTests.cs`.
+
+**Checkpoint**: Shared contracts, options, permissions, features, retention controls, tool enablement, durable proposal storage, and durable audit storage are ready for story work.
+
+---
+
+## Phase 3: User Story 1 - Chat with workflow-aware Weaver (Priority: P1) MVP
+
+**Goal**: Authorized users can chat with Weaver, attach references, receive server-owned streaming assistant and tool events, and recover durable outputs after a short disconnect.
+
+**Independent Test**: Start a chat with a workflow definition reference, verify server-side context resolution, assistant deltas, tool lifecycle events, reconnect within grace window, and no direct Studio/provider calls.
+
+### Tests for User Story 1
+
+- [X] T045 [P] [US1] Add context authorization tests in `test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs`.
+- [X] T046 [P] [US1] Add stream event mapping tests in `test/unit/Elsa.AI.Host.UnitTests/Streaming/AIStreamEventMapperTests.cs`.
+- [X] T047 [P] [US1] Add reconnect grace tests in `test/unit/Elsa.AI.Host.UnitTests/Streaming/AIReconnectGraceTests.cs`.
+- [X] T048 [P] [US1] Add chat endpoint streaming integration tests in `test/integration/Elsa.AI.IntegrationTests/AIChatEndpointTests.cs`.
+- [X] T049 [P] [US1] Add capabilities endpoint integration tests in `test/integration/Elsa.AI.IntegrationTests/AICapabilitiesEndpointTests.cs`.
+- [X] T050 [P] [US1] Add tools endpoint integration tests in `test/integration/Elsa.AI.IntegrationTests/AIToolsEndpointTests.cs`.
+- [X] T051 [P] [US1] Add disconnected chat recovery integration tests in `test/integration/Elsa.AI.IntegrationTests/AIChatReconnectTests.cs`.
+
+### Implementation for User Story 1
+
+- [X] T052 [P] [US1] Implement workflow definition context provider in `src/modules/Elsa.AI.Host/Context/WorkflowDefinitionContextProvider.cs`.
+- [X] T053 [P] [US1] Implement workflow instance context provider in `src/modules/Elsa.AI.Host/Context/WorkflowInstanceContextProvider.cs`.
+- [X] T054 [US1] Implement context resolver with authorization and redaction in `src/modules/Elsa.AI.Host/Context/AIContextResolver.cs`.
+- [X] T055 [US1] Implement AI tool registry with enablement filtering in `src/modules/Elsa.AI.Host/Services/AIToolRegistry.cs`.
+- [X] T056 [US1] Implement orchestrator turn flow in `src/modules/Elsa.AI.Host/Services/AIOrchestrator.cs`.
+- [X] T057 [US1] Implement reconnect grace tracking in `src/modules/Elsa.AI.Host/Streaming/AIStreamSessionManager.cs`.
+- [X] T058 [US1] Implement stream event mapper in `src/modules/Elsa.AI.Host/Streaming/AIStreamEventMapper.cs`.
+- [X] T059 [US1] Implement chat endpoint in `src/modules/Elsa.AI.Host/Endpoints/AI/Chat/Endpoint.cs`.
+- [X] T060 [US1] Implement tools endpoint in `src/modules/Elsa.AI.Host/Endpoints/AI/Tools/Endpoint.cs`.
+- [X] T061 [US1] Implement capabilities endpoint in `src/modules/Elsa.AI.Host/Endpoints/AI/Capabilities/Endpoint.cs`.
+- [X] T062 [US1] Implement Copilot provider event adapter in `src/modules/Elsa.AI.Copilot/Adapters/CopilotProvider.cs`.
+- [X] T063 [US1] Implement Copilot feature and shell feature registration in `src/modules/Elsa.AI.Copilot/ShellFeatures/CopilotAIFeature.cs`.
+- [X] T064 [US1] Draft paired Studio Razor chat panel implementation, after reviewing the `elsa-extensions` `origin/feat/ai` UI prototype at commit `93f0e09d71e57f5daff1e2d593f0a51faaa80417`, in `../elsa-studio/src/modules/Elsa.Studio.AI/UI/Components/WeaverChatPanel.razor`.
+- [X] T065 [US1] Run chat MVP tests in `test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj`.
+
+**Checkpoint**: Weaver chat, context references, stream events, reconnect recovery, capabilities, and tool listing work as the MVP.
+
+---
+
+## Phase 4: User Story 2 - Generate workflows through reviewable proposals (Priority: P2)
+
+**Goal**: Users can generate durable workflow proposals and apply them only after explicit validation, approval, authorization, and durable audit.
+
+**Independent Test**: Ask Weaver to create a workflow, inspect the durable proposal, verify blocked direct mutation, approve and apply as the same authorized user, restart storage, and verify audit/proposal records survive.
+
+### Tests for User Story 2
+
+- [ ] T066 [P] [US2] Add proposal lifecycle tests in `test/unit/Elsa.AI.Host.UnitTests/Proposals/AIProposalLifecycleTests.cs`.
+- [ ] T067 [P] [US2] Add same-authorized-user approval tests in `test/unit/Elsa.AI.Host.UnitTests/Proposals/AIProposalApprovalTests.cs`.
+- [ ] T068 [P] [US2] Add workflow draft validation tests in `test/unit/Elsa.AI.Host.UnitTests/Tools/WorkflowValidateDraftToolTests.cs`.
+- [ ] T069 [P] [US2] Add proposal apply endpoint tests in `test/integration/Elsa.AI.IntegrationTests/AIProposalApplyEndpointTests.cs`.
+- [ ] T070 [P] [US2] Add durable proposal restart integration tests in `test/integration/Elsa.AI.IntegrationTests/AIProposalPersistenceTests.cs`.
+- [ ] T071 [P] [US2] Add durable proposal audit integration tests in `test/integration/Elsa.AI.IntegrationTests/AIProposalAuditTests.cs`.
+
+### Implementation for User Story 2
+
+- [ ] T072 [P] [US2] Implement proposal lifecycle service in `src/modules/Elsa.AI.Host/Proposals/AIProposalService.cs`.
+- [ ] T073 [P] [US2] Implement workflow draft validator adapter in `src/modules/Elsa.AI.Host/Proposals/WorkflowDraftValidator.cs`.
+- [ ] T074 [US2] Implement `workflow.proposeCreate` tool in `src/modules/Elsa.AI.Host/Tools/Workflow/WorkflowProposeCreateTool.cs`.
+- [ ] T075 [US2] Implement `workflow.validateDraft` tool in `src/modules/Elsa.AI.Host/Tools/Workflow/WorkflowValidateDraftTool.cs`.
+- [ ] T076 [US2] Implement proposal details endpoint in `src/modules/Elsa.AI.Host/Endpoints/AI/Proposals/Get/Endpoint.cs`.
+- [ ] T077 [US2] Implement proposal approve endpoint in `src/modules/Elsa.AI.Host/Endpoints/AI/Proposals/Approve/Endpoint.cs`.
+- [ ] T078 [US2] Implement proposal reject endpoint in `src/modules/Elsa.AI.Host/Endpoints/AI/Proposals/Reject/Endpoint.cs`.
+- [ ] T079 [US2] Implement proposal apply endpoint in `src/modules/Elsa.AI.Host/Endpoints/AI/Proposals/Apply/Endpoint.cs`.
+- [ ] T080 [US2] Add stale baseline checks in `src/modules/Elsa.AI.Host/Proposals/WorkflowProposalApplier.cs`.
+- [ ] T081 [US2] Add durable proposal audit events in `src/modules/Elsa.AI.Host/Proposals/AIProposalAuditService.cs`.
+- [ ] T082 [US2] Add proposal stream notifications in `src/modules/Elsa.AI.Host/Streaming/AIStreamEventMapper.cs`.
+- [ ] T083 [US2] Draft paired Studio Razor proposal viewer using applicable table/dialog conventions from the `elsa-extensions` AI prototype in `../elsa-studio/src/modules/Elsa.Studio.AI/UI/Components/WeaverProposalViewer.razor`.
+- [ ] T084 [US2] Run proposal tests in `test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj`.
+
+**Checkpoint**: AI-generated workflow creation is durable, proposal-only, reviewable, validated, applicable, and audited.
+
+---
+
+## Phase 5: User Story 3 - Edit and validate existing workflows safely (Priority: P3)
+
+**Goal**: Users can ask Weaver to explain, modify, and validate existing workflows through durable reviewable diffs and proposals.
+
+**Independent Test**: Attach an existing workflow, request a targeted change, verify graph diff, warnings, validation, durable proposal storage, and proposal-only apply.
+
+### Tests for User Story 3
+
+- [ ] T085 [P] [US3] Add workflow explain tool tests in `test/unit/Elsa.AI.Host.UnitTests/Tools/WorkflowExplainToolTests.cs`.
+- [ ] T086 [P] [US3] Add workflow update proposal tests in `test/unit/Elsa.AI.Host.UnitTests/Tools/WorkflowProposeUpdateToolTests.cs`.
+- [ ] T087 [P] [US3] Add graph diff tests in `test/unit/Elsa.AI.Host.UnitTests/Proposals/WorkflowGraphDiffTests.cs`.
+- [ ] T088 [P] [US3] Add update proposal persistence tests in `test/integration/Elsa.AI.IntegrationTests/AIWorkflowUpdateProposalPersistenceTests.cs`.
+
+### Implementation for User Story 3
+
+- [ ] T089 [P] [US3] Implement `workflow.getDefinition` tool in `src/modules/Elsa.AI.Host/Tools/Workflow/WorkflowGetDefinitionTool.cs`.
+- [ ] T090 [P] [US3] Implement `workflow.listDefinitions` tool in `src/modules/Elsa.AI.Host/Tools/Workflow/WorkflowListDefinitionsTool.cs`.
+- [ ] T091 [US3] Implement `workflow.explain` tool in `src/modules/Elsa.AI.Host/Tools/Workflow/WorkflowExplainTool.cs`.
+- [ ] T092 [US3] Implement `workflow.proposeUpdate` tool in `src/modules/Elsa.AI.Host/Tools/Workflow/WorkflowProposeUpdateTool.cs`.
+- [ ] T093 [US3] Implement `workflow.validateChanges` tool in `src/modules/Elsa.AI.Host/Tools/Workflow/WorkflowValidateChangesTool.cs`.
+- [ ] T094 [US3] Implement graph diff builder in `src/modules/Elsa.AI.Host/Proposals/WorkflowGraphDiffBuilder.cs`.
+- [ ] T095 [US3] Add update proposal apply handling in `src/modules/Elsa.AI.Host/Proposals/WorkflowProposalApplier.cs`.
+- [ ] T096 [US3] Draft paired Studio Razor graph diff view using applicable tabbed-editor conventions from the `elsa-extensions` AI prototype in `../elsa-studio/src/modules/Elsa.Studio.AI/UI/Components/WeaverGraphDiff.razor`.
+- [ ] T097 [US3] Add breaking-change warning surfaces in `src/modules/Elsa.AI.Host/Proposals/WorkflowProposalWarnings.cs`.
+- [ ] T098 [US3] Run workflow editing tests in `test/unit/Elsa.AI.Host.UnitTests/Elsa.AI.Host.UnitTests.csproj`.
+
+**Checkpoint**: Existing workflows can be explained, edited through durable proposals, validated, diffed, and safely applied.
+
+---
+
+## Phase 6: User Story 4 - Analyze runtime incidents conversationally (Priority: P4)
+
+**Goal**: Operators can ask Weaver to inspect runtime failures, summarize incidents, identify failing activities, and detect trends within attached references plus an explicit time range and diagnostics scope.
+
+**Independent Test**: Attach failed instance and diagnostics references, select a time range/scope, ask for incident analysis and trends, and verify grounded evidence, scope boundaries, redaction, and read-only tool behavior.
+
+### Tests for User Story 4
+
+- [ ] T099 [P] [US4] Add instance get tool tests in `test/unit/Elsa.AI.Host.UnitTests/Tools/InstanceGetToolTests.cs`.
+- [ ] T100 [P] [US4] Add failure aggregation tests in `test/unit/Elsa.AI.Host.UnitTests/Tools/InstanceAggregateFailuresToolTests.cs`.
+- [ ] T101 [P] [US4] Add diagnostics redaction tests in `test/unit/Elsa.AI.Host.UnitTests/Context/DiagnosticsContextRedactionTests.cs`.
+- [ ] T102 [P] [US4] Add trend scope enforcement tests in `test/unit/Elsa.AI.Host.UnitTests/Tools/InstanceDetectTrendsScopeTests.cs`.
+- [ ] T103 [P] [US4] Add incident summary integration tests in `test/integration/Elsa.AI.IntegrationTests/AIRuntimeDiagnosticsTests.cs`.
+
+### Implementation for User Story 4
+
+- [ ] T104 [P] [US4] Implement `instance.get` tool in `src/modules/Elsa.AI.Host/Tools/Instances/InstanceGetTool.cs`.
+- [ ] T105 [P] [US4] Implement `instance.search` tool in `src/modules/Elsa.AI.Host/Tools/Instances/InstanceSearchTool.cs`.
+- [ ] T106 [US4] Implement `instance.getErrors` tool in `src/modules/Elsa.AI.Host/Tools/Instances/InstanceGetErrorsTool.cs`.
+- [ ] T107 [US4] Implement `instance.aggregateFailures` tool in `src/modules/Elsa.AI.Host/Tools/Instances/InstanceAggregateFailuresTool.cs`.
+- [ ] T108 [US4] Implement scoped `instance.detectTrends` tool in `src/modules/Elsa.AI.Host/Tools/Instances/InstanceDetectTrendsTool.cs`.
+- [ ] T109 [US4] Implement `instance.summarize` tool in `src/modules/Elsa.AI.Host/Tools/Instances/InstanceSummarizeTool.cs`.
+- [ ] T110 [US4] Implement diagnostics context provider in `src/modules/Elsa.AI.Host/Context/DiagnosticsContextProvider.cs`.
+- [ ] T111 [US4] Implement diagnostics scope resolver in `src/modules/Elsa.AI.Host/Context/DiagnosticsScopeResolver.cs`.
+- [ ] T112 [US4] Draft paired Studio Razor diagnostics timeline using applicable `/ai/*` route and dense table conventions from the `elsa-extensions` AI prototype in `../elsa-studio/src/modules/Elsa.Studio.AI/UI/Components/WeaverDiagnosticsTimeline.razor`.
+- [ ] T113 [US4] Run runtime diagnostics tests in `test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj`.
+
+**Checkpoint**: Weaver can analyze failures and scoped trends conversationally without mutating runtime state or reading outside the selected scope.
+
+---
+
+## Phase 7: User Story 5 - Extend Weaver with governed tools and agents (Priority: P5)
+
+**Goal**: Third-party modules can register tools, context providers, custom agents, and MCP integrations with enforceable governance metadata and explicit enablement for higher-risk tools.
+
+**Independent Test**: Register a module-provided read-only tool and proposal/MCP tool, verify read-only availability, explicit enablement requirements, authorization denial, audit, and agent tool scoping.
+
+### Tests for User Story 5
+
+- [ ] T114 [P] [US5] Add extension registration tests in `test/unit/Elsa.AI.Host.UnitTests/Extensions/AIFeatureExtensionsTests.cs`.
+- [ ] T115 [P] [US5] Add agent scoping tests in `test/unit/Elsa.AI.Host.UnitTests/Agents/AIAgentScopeTests.cs`.
+- [ ] T116 [P] [US5] Add MCP registration tests in `test/unit/Elsa.AI.Host.UnitTests/Mcp/AIMcpRegistrationTests.cs`.
+- [ ] T117 [P] [US5] Add explicit enablement tests in `test/unit/Elsa.AI.Host.UnitTests/Tools/AIToolEnablementTests.cs`.
+- [ ] T118 [P] [US5] Add tool authorization denial audit tests in `test/integration/Elsa.AI.IntegrationTests/AIToolGovernanceTests.cs`.
+
+### Implementation for User Story 5
+
+- [ ] T119 [P] [US5] Implement AI feature extension APIs in `src/modules/Elsa.AI.Host/Extensions/AIFeatureExtensions.cs`.
+- [ ] T120 [P] [US5] Add agent definition contracts in `src/modules/Elsa.AI.Abstractions/Contracts/IAIAgentDefinitionProvider.cs`.
+- [ ] T121 [US5] Implement agent registry in `src/modules/Elsa.AI.Host/Services/AIAgentRegistry.cs`.
+- [ ] T122 [US5] Implement agent-scoped tool resolver in `src/modules/Elsa.AI.Host/Services/AIAgentToolResolver.cs`.
+- [ ] T123 [US5] Implement MCP registration models in `src/modules/Elsa.AI.Abstractions/Models/AIMcpServerRegistration.cs`.
+- [ ] T124 [US5] Implement MCP registry in `src/modules/Elsa.AI.Host/Services/AIMcpServerRegistry.cs`.
+- [ ] T125 [US5] Implement explicit enablement persistence in `src/modules/Elsa.AI.Host/Services/AIToolEnablementStore.cs`.
+- [ ] T126 [US5] Map agent and MCP scopes to Copilot session config in `src/modules/Elsa.AI.Copilot/Adapters/CopilotSessionFactory.cs`.
+- [ ] T127 [US5] Implement built-in `workflow-author` agent provider in `src/modules/Elsa.AI.Host/Agents/WorkflowAuthorAgentProvider.cs`.
+- [ ] T128 [US5] Implement built-in `instance-diagnostics` agent provider in `src/modules/Elsa.AI.Host/Agents/InstanceDiagnosticsAgentProvider.cs`.
+- [ ] T129 [US5] Add extension authoring README in `src/modules/Elsa.AI.Host/README.md`.
+- [ ] T130 [US5] Run extensibility tests in `test/unit/Elsa.AI.Host.UnitTests/Elsa.AI.Host.UnitTests.csproj`.
+
+**Checkpoint**: Modules can extend Weaver without weakening tool scope, explicit enablement, tenant enforcement, or audit.
+
+---
+
+## Phase 8: Cross-Cutting Verification, Documentation & Polish
+
+**Purpose**: Close cross-cutting governance gaps, validate boundaries, update documentation, and run targeted checks.
+
+- [ ] T131 [P] Update implementation quickstart notes in `specs/008-weaver-ai-copilot/quickstart.md`.
+- [ ] T132 [P] Add API documentation in `src/modules/Elsa.AI.Host/README.md`.
+- [ ] T133 [P] Add Copilot adapter documentation in `src/modules/Elsa.AI.Copilot/README.md`.
+- [ ] T134 [P] Add persistence provider documentation in `src/modules/Elsa.AI.Persistence.EFCore/README.md`.
+- [ ] T135 Add provider leakage boundary test in `test/integration/Elsa.AI.IntegrationTests/AIProviderIsolationTests.cs`.
+- [ ] T136 Add durable governance restart test in `test/integration/Elsa.AI.IntegrationTests/AIDurableGovernanceTests.cs`.
+- [ ] T137 Run abstractions tests in `test/unit/Elsa.AI.Abstractions.UnitTests/Elsa.AI.Abstractions.UnitTests.csproj`.
+- [ ] T138 Run host tests in `test/unit/Elsa.AI.Host.UnitTests/Elsa.AI.Host.UnitTests.csproj`.
+- [ ] T139 Run Copilot adapter tests in `test/unit/Elsa.AI.Copilot.UnitTests/Elsa.AI.Copilot.UnitTests.csproj`.
+- [ ] T140 Run EF Core persistence tests in `test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj`.
+- [ ] T141 Run integration tests in `test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj`.
+- [ ] T142 Run provider boundary search over `src/modules/Elsa.AI.Abstractions` and `src/modules/Elsa.AI.Host`.
+- [ ] T143 Compare final Studio UI plan against the `elsa-extensions` AI prototype and document carried-forward and rejected prototype patterns in `../elsa-studio/src/modules/Elsa.Studio.AI/README.md`.
+- [ ] T144 Add provider/BYOK configuration registry tests in `test/unit/Elsa.AI.Host.UnitTests/Providers/AIProviderConfigurationRegistryTests.cs`.
+- [ ] T145 Implement provider/BYOK configuration registry with secret-reference handling in `src/modules/Elsa.AI.Host/Services/AIProviderConfigurationRegistry.cs`.
+- [ ] T146 Add OpenTelemetry activity and metric definitions for chat, tools, proposals, and provider calls in `src/modules/Elsa.AI.Host/Telemetry/AITelemetry.cs`.
+- [ ] T147 Add OpenTelemetry instrumentation tests in `test/unit/Elsa.AI.Host.UnitTests/Telemetry/AITelemetryTests.cs`.
+- [ ] T148 Add redaction boundary tests for model context, tool results, proposal rationale, stream events, and audit records in `test/unit/Elsa.AI.Host.UnitTests/Redaction/AIRedactionBoundaryTests.cs`.
+- [ ] T149 Implement centralized AI redaction boundary service in `src/modules/Elsa.AI.Host/Services/AIRedactionService.cs`.
+- [ ] T150 Add prompt, session, model event, and tool invocation audit tests in `test/integration/Elsa.AI.IntegrationTests/AISessionAuditTests.cs`.
+- [ ] T151 Add configurable conversation/session retention expiration tests in `test/unit/Elsa.AI.Host.UnitTests/Conversations/AIConversationRetentionTests.cs`.
+- [ ] T152 Add latency smoke tests for first stream event, capabilities, and proposal validation in `test/integration/Elsa.AI.IntegrationTests/AILatencyTests.cs`.
+- [ ] T153 Document Core and Studio implementation worktree setup in `specs/008-weaver-ai-copilot/quickstart.md`.
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Phase 1 Setup**: No dependencies.
+- **Phase 2 Foundational**: Depends on Phase 1 and blocks all user stories.
+- **Phase 3 US1**: Depends on Phase 2; MVP chat slice.
+- **Phase 4 US2**: Depends on Phase 2 and should follow US1 for chat/proposal stream notifications.
+- **Phase 5 US3**: Depends on Phase 4 proposal lifecycle for update proposals.
+- **Phase 6 US4**: Depends on Phase 2 and can proceed after US1 tool streaming is stable.
+- **Phase 7 US5**: Depends on Phase 2 and should follow US1 tool execution semantics.
+- **Phase 8 Cross-Cutting Verification**: Documentation and final test tasks depend on selected story implementation; provider, telemetry, redaction, audit, retention, latency, and worktree tasks should be pulled forward into the earliest story phase that needs them.
+
+### User Story Dependencies
+
+- **US1 (P1)**: First executable slice; depends only on foundational contracts and stores.
+- **US2 (P2)**: Uses US1 streaming and foundational durable proposal/audit storage.
+- **US3 (P3)**: Uses US2 proposal lifecycle and adds update/diff behavior.
+- **US4 (P4)**: Uses US1 chat/tool execution and adds scoped read-only diagnostics tools.
+- **US5 (P5)**: Uses foundational registry contracts and validates third-party extensibility.
+
+### Parallel Opportunities
+
+- T001 through T009 and T011 through T016 can run in parallel.
+- T017 through T027 and T031 through T036 can run in parallel after project creation.
+- US1 tests T045 through T051 can run in parallel.
+- US2 tests T066 through T071 can run in parallel.
+- US3 tests T085 through T088 can run in parallel.
+- US4 tests T099 through T103 can run in parallel.
+- US5 tests T114 through T118 can run in parallel.
+- Documentation tasks T131 through T134 can run in parallel after APIs settle.
+- Cross-cutting tests T144, T147, T148, T150, T151, and T152 can run in parallel after foundational contracts exist.
+
+## Parallel Example: User Story 1
+
+```text
+Task: "Add context authorization tests in test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs"
+Task: "Add stream event mapping tests in test/unit/Elsa.AI.Host.UnitTests/Streaming/AIStreamEventMapperTests.cs"
+Task: "Add reconnect grace tests in test/unit/Elsa.AI.Host.UnitTests/Streaming/AIReconnectGraceTests.cs"
+Task: "Add chat endpoint streaming integration tests in test/integration/Elsa.AI.IntegrationTests/AIChatEndpointTests.cs"
+```
+
+## Parallel Example: User Story 2
+
+```text
+Task: "Add proposal lifecycle tests in test/unit/Elsa.AI.Host.UnitTests/Proposals/AIProposalLifecycleTests.cs"
+Task: "Add same-authorized-user approval tests in test/unit/Elsa.AI.Host.UnitTests/Proposals/AIProposalApprovalTests.cs"
+Task: "Add durable proposal restart integration tests in test/integration/Elsa.AI.IntegrationTests/AIProposalPersistenceTests.cs"
+Task: "Add durable proposal audit integration tests in test/integration/Elsa.AI.IntegrationTests/AIProposalAuditTests.cs"
+```
+
+## Parallel Example: User Story 4
+
+```text
+Task: "Add failure aggregation tests in test/unit/Elsa.AI.Host.UnitTests/Tools/InstanceAggregateFailuresToolTests.cs"
+Task: "Add diagnostics redaction tests in test/unit/Elsa.AI.Host.UnitTests/Context/DiagnosticsContextRedactionTests.cs"
+Task: "Add trend scope enforcement tests in test/unit/Elsa.AI.Host.UnitTests/Tools/InstanceDetectTrendsScopeTests.cs"
+Task: "Add incident summary integration tests in test/integration/Elsa.AI.IntegrationTests/AIRuntimeDiagnosticsTests.cs"
+```
+
+## Implementation Strategy
+
+### MVP First
+
+1. Complete Phase 1 and Phase 2, including durable proposal and audit persistence.
+2. Complete Phase 3 only.
+3. Run US1 unit and integration tests.
+4. Stop and validate server-hosted chat, context references, capabilities, tool listing, streaming deltas, tool progress, reconnect recovery, and provider isolation.
+
+### Incremental Delivery
+
+1. Add US2 to enable durable safe workflow creation proposals.
+2. Add US3 to enable workflow edits and graph diffs.
+3. Add US4 to enable scoped runtime intelligence.
+4. Add US5 to open governed third-party extension points.
+5. Run Phase 8 validation before PR handoff.
+
+### Paired Studio Work
+
+Studio implementation tasks reference `../elsa-studio/` because this workspace is `elsa-core`. If the Studio repository is not checked out beside this repository, complete the server/Core tasks first and carry the Studio tasks into the sibling repository.
+
+Review the `../elsa-extensions-investigation` `origin/feat/ai` prototype before Studio work. Use route/menu/table/form conventions where they help, but keep chat/proposal review primary and avoid raw API key reveal or provider JSON configuration as the main UX.
diff --git a/specs/011-persistence-vnext/checklists/requirements.md b/specs/011-persistence-vnext/checklists/requirements.md
new file mode 100644
index 000000000..98259214a
--- /dev/null
+++ b/specs/011-persistence-vnext/checklists/requirements.md
@@ -0,0 +1,34 @@
+# Specification Quality Checklist: Persistence vNext
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-06-02
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- Specification is ready for implementation planning.
diff --git a/specs/011-persistence-vnext/roadmap.md b/specs/011-persistence-vnext/roadmap.md
new file mode 100644
index 000000000..91fd4ddb3
--- /dev/null
+++ b/specs/011-persistence-vnext/roadmap.md
@@ -0,0 +1,228 @@
+# Persistence vNext Executable Roadmap
+
+Parent issue: [#7580](https://github.com/elsa-workflows/elsa-core/issues/7580)
+
+## S1 - Core Manifest And Planner Foundations
+
+Outcome: Modules can declare provider-neutral storage intent once, and relational/document planners can consume that same manifest.
+
+Scope:
+
+- Core descriptors.
+- Provider extension contracts.
+- Relational planner.
+- Document planner.
+- Secrets sample manifest.
+- SQLite and SQL Server render proof.
+
+Non-scope:
+
+- Full document store.
+- Runtime entity UI.
+- Production provider packages.
+
+Acceptance criteria:
+
+- Same Secrets manifest produces relational and document plans.
+- SQLite DDL can execute.
+- SQL Server DDL can render.
+- Schema history records applied version.
+
+Validation:
+
+- `dotnet test test/unit/Elsa.Persistence.VNext.UnitTests/Elsa.Persistence.VNext.UnitTests.csproj`
+
+GitHub issue mapping: [#7670](https://github.com/elsa-workflows/elsa-core/issues/7670)
+
+## S2 - Portable SQLite Document Store MVP
+
+Outcome: A provider-neutral document store can save, load, delete, index, and query documents on SQLite.
+
+Scope:
+
+- Document envelope.
+- SQLite document table.
+- Generic field index table.
+- Save/load/delete.
+- Index maintenance.
+- Basic query by declared indexes.
+- Optimistic concurrency.
+
+Non-scope:
+
+- SQL Server/MongoDB providers.
+- Runtime entity admin UI.
+- Physicalization policy.
+
+Acceptance criteria:
+
+- Documents can be saved and loaded.
+- Indexed fields are maintained transactionally.
+- Queries by declared indexes return expected documents.
+- Unindexed queries fail clearly.
+
+Validation:
+
+- Targeted unit and SQLite integration tests.
+
+GitHub issue mapping: [#7671](https://github.com/elsa-workflows/elsa-core/issues/7671)
+
+## S3 - SQL Server And PostgreSQL Relational Document Providers
+
+Outcome: The portable document/index contract runs on SQL Server and PostgreSQL.
+
+Scope:
+
+- SQL Server document store.
+- PostgreSQL document store.
+- Generic field indexes.
+- Startup locking/materialization.
+- Provider integration tests.
+
+Non-scope:
+
+- MongoDB.
+- Runtime entity UI.
+- Advanced physicalization.
+
+Acceptance criteria:
+
+- Same document/index contract tests pass on SQLite, SQL Server, and PostgreSQL.
+- Provider differences remain isolated to provider packages.
+
+GitHub issue mapping: [#7672](https://github.com/elsa-workflows/elsa-core/issues/7672)
+
+Local progress note: provider packages and SQL dialect tests are implemented locally. Live SQL Server/PostgreSQL contract tests require a running Docker/Testcontainers environment or CI worker.
+
+## S4 - MongoDB Document Provider
+
+Outcome: The same storage/index model works on MongoDB using native collections and indexes.
+
+Scope:
+
+- MongoDB document store.
+- Native collection/index materialization.
+- Query translation for declared indexes.
+- Optimistic concurrency.
+- Provider capability validation.
+
+Non-scope:
+
+- Aggregation pipelines beyond portable query contract.
+- Runtime entity UI.
+
+Acceptance criteria:
+
+- Same document/index contract tests pass on MongoDB.
+- Native indexes are created and used for declared indexes.
+
+GitHub issue mapping: [#7673](https://github.com/elsa-workflows/elsa-core/issues/7673)
+
+Local progress note: MongoDB provider package, native collection/index planner, query translation, and expected-version write/delete filters are implemented locally. Live MongoDB contract tests require a running MongoDB service or CI worker.
+
+## S5 - Elsa Integration And First Module Migration
+
+Outcome: Elsa consumes vNext persistence and one real module no longer needs EF Core migrations.
+
+Scope:
+
+- Elsa registration package.
+- Shell feature/options.
+- Startup materialization.
+- Diagnostics.
+- Secrets module vNext store.
+- EF Core import path if required.
+
+Non-scope:
+
+- Workflow runtime hot paths.
+- Runtime-defined entities.
+
+Acceptance criteria:
+
+- Secrets works on vNext persistence.
+- Provider-specific EF Core migration packages are not required for Secrets vNext.
+- Existing behavior is preserved.
+
+GitHub issue mapping: [#7674](https://github.com/elsa-workflows/elsa-core/issues/7674)
+
+Local progress note: opt-in Elsa integration package, startup materialization task, status snapshot, and Secrets vNext repository are implemented locally. Existing Secrets persistence remains intact; import/migration from existing stores is intentionally left open until the storage model is proven further.
+
+## S6 - Runtime-Defined Entities
+
+Outcome: Admin-defined entities can be published, persisted, and queried through the document/index model.
+
+Scope:
+
+- Runtime entity definitions.
+- Draft/published/retired lifecycle.
+- Validation.
+- Index definitions.
+- CRUD/API integration.
+- Audit trail.
+
+Non-scope:
+
+- Full Studio UI polish.
+- Native physicalization.
+
+Acceptance criteria:
+
+- Admin-defined entity can be published.
+- Instances can be created and queried by indexed fields.
+- No physical table per runtime entity is required by default.
+
+GitHub issue mapping: [#7675](https://github.com/elsa-workflows/elsa-core/issues/7675)
+
+Local progress note: runtime entity definitions, draft/published/retired lifecycle, validation, fixed-slot indexed instance storage, service-level CRUD/query API, and audit records are implemented locally. The POC intentionally does not add Studio UI or HTTP endpoints yet.
+
+## S7 - Physicalization And Performance
+
+Outcome: Hot entities and indexes can opt into provider-optimized physical storage.
+
+Scope:
+
+- Storage policies.
+- Optimized indexes.
+- Native physicalization hooks.
+- Benchmarks.
+- Provider capability validation.
+
+Non-scope:
+
+- Universal auto-optimization.
+
+Acceptance criteria:
+
+- At least one relational provider supports optimized indexes.
+- At least one document provider supports provider-native optimization.
+- Portable default remains unchanged.
+
+GitHub issue mapping: [#7676](https://github.com/elsa-workflows/elsa-core/issues/7676)
+
+Local progress note: provider-neutral physicalization policies and manifest validation are implemented locally. SQLite can plan dedicated table/index DDL for hot storage units, and MongoDB can plan dedicated collection/native-index operations. Real workload benchmarks remain open.
+
+## S8 - Workflow Runtime Evaluation And Hardening
+
+Outcome: vNext is evaluated against workflow runtime hot paths and hardened for production.
+
+Scope:
+
+- Runtime store benchmark.
+- Concurrency/locking validation.
+- Retry and diagnostics.
+- Backup/recovery guidance.
+- Provider status reporting.
+
+Non-scope:
+
+- Premature runtime migration without benchmark evidence.
+
+Acceptance criteria:
+
+- Clear go/no-go decision for workflow runtime stores.
+- Production operational risks are documented and mitigated.
+
+GitHub issue mapping: [#7677](https://github.com/elsa-workflows/elsa-core/issues/7677)
+
+Local progress note: a workflow runtime evaluator and go/no-go design note are implemented locally. Metadata/module stores are classified as good vNext candidates, hot lookup stores are benchmark-gated, and queue/log workloads remain specialized-provider candidates. Real provider benchmarks and concurrency/locking contract tests remain open.
diff --git a/specs/011-persistence-vnext/spec.md b/specs/011-persistence-vnext/spec.md
new file mode 100644
index 000000000..0a23acc6b
--- /dev/null
+++ b/specs/011-persistence-vnext/spec.md
@@ -0,0 +1,169 @@
+# Feature Specification: Persistence vNext
+
+**Feature Branch**: `codex/persistence-vnext-poc`
+**Created**: 2026-06-02
+**Status**: Draft
+**Input**: User description: "Execute the Persistence vNext roadmap end to end: provider-neutral modular persistence for Elsa, with relational and document database support, runtime-defined entities, portable document/index storage by default, provider-optimized physicalization by policy, PR review/merge flow, and issue completion tracking."
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Module-Owned Storage Intent (Priority: P1)
+
+As an Elsa module author, I want to declare the storage shape and query indexes my module needs once, without referencing SQL Server, SQLite, EF Core, MongoDB, or any provider-specific migration package.
+
+**Why this priority**: This removes the current per-module/per-provider migration maintenance burden and establishes the foundation for every later slice.
+
+**Independent Test**: A module can register a versioned storage manifest, and separate relational and document planners can produce provider-family plans from the same manifest.
+
+**Acceptance Scenarios**:
+
+1. **Given** a module manifest with a storage unit, fields, key, and indexes, **When** a relational planner processes it, **Then** it produces a table/index plan without requiring provider-specific module code.
+2. **Given** the same module manifest, **When** a document planner processes it, **Then** it produces a collection/index plan without requiring relational descriptors.
+
+---
+
+### User Story 2 - Provider-Owned Materialization (Priority: P1)
+
+As an Elsa operator, I want the selected persistence provider to materialize schema/indexes from module manifests so modules do not ship provider-specific migrations.
+
+**Why this priority**: This is the central replacement for EF Core migrations as schema authority.
+
+**Independent Test**: SQLite can execute generated schema/index operations and record the applied schema version; SQL Server can render provider-specific DDL from the same manifest.
+
+**Acceptance Scenarios**:
+
+1. **Given** a manifest version that has not been applied, **When** SQLite startup materialization runs, **Then** required tables/indexes are created and the version is recorded.
+2. **Given** the same manifest version already recorded, **When** SQLite startup materialization runs again, **Then** no schema operations are re-applied.
+
+---
+
+### User Story 3 - Portable Document Store (Priority: P1)
+
+As an Elsa module author, I want to persist and query module documents using declared indexes, so simple modules do not need custom EF Core, SQL, or MongoDB code.
+
+**Why this priority**: A YesSQL-like document/index model is the practical way to support both relational and native document databases.
+
+**Independent Test**: A document can be saved, loaded, deleted, and queried by declared indexed fields on SQLite.
+
+**Acceptance Scenarios**:
+
+1. **Given** a document type with indexed fields, **When** the document is saved, **Then** provider-owned index records are maintained.
+2. **Given** a query against a declared index, **When** the query executes, **Then** matching documents are returned with paging and sorting where supported.
+3. **Given** a query against an undeclared index, **When** the query is validated, **Then** it fails with a clear provider-neutral error.
+
+---
+
+### User Story 4 - Native Document Provider Support (Priority: P2)
+
+As an Elsa deployer, I want to run the same module persistence model on MongoDB so Elsa remains database agnostic.
+
+**Why this priority**: The abstraction is not successful unless document databases can use native collections/indexes rather than relational tables.
+
+**Independent Test**: The same document/index contract passes against MongoDB with native index creation.
+
+**Acceptance Scenarios**:
+
+1. **Given** a storage manifest, **When** MongoDB materialization runs, **Then** collections and native indexes are created.
+2. **Given** indexed document queries, **When** they execute against MongoDB, **Then** they use the provider's native query/index model.
+
+---
+
+### User Story 5 - Runtime-Defined Entities (Priority: P2)
+
+As an Elsa administrator, I want to define entities at runtime and query them by declared indexed fields without creating a physical table per entity by default.
+
+**Why this priority**: Runtime-defined entities unlock admin-defined domain concepts while preserving provider portability.
+
+**Independent Test**: An admin-defined entity definition can be published, materialized, used for CRUD, and queried by declared indexes.
+
+**Acceptance Scenarios**:
+
+1. **Given** a draft entity definition, **When** it is validated and published, **Then** the selected provider materializes required indexes.
+2. **Given** an instance of that runtime entity, **When** it is saved, **Then** it is stored as a document and queryable by declared indexes.
+
+---
+
+### User Story 6 - Provider-Optimized Physicalization (Priority: P3)
+
+As an Elsa operator, I want to opt hot entities or indexes into provider-optimized physical shapes without changing module code.
+
+**Why this priority**: Portable defaults must not block high-performance production workloads.
+
+**Independent Test**: A storage policy can be changed from portable document storage to optimized indexes or native physicalization, and the provider reports what physical changes will be applied.
+
+**Acceptance Scenarios**:
+
+1. **Given** a hot index, **When** an optimized index policy is selected, **Then** the provider materializes a stronger provider-specific index.
+2. **Given** unsupported physicalization, **When** validation runs, **Then** the provider reports the unsupported capability before applying changes.
+
+### Edge Cases
+
+- Multiple app instances start concurrently and attempt materialization.
+- A schema version is already applied.
+- A provider does not support a requested index or field type.
+- A runtime entity definition is changed while existing documents exist.
+- A query references a field that is not indexed.
+- A physicalization policy is unsupported by the selected provider.
+- A document save succeeds but index maintenance fails.
+- MongoDB transaction support is unavailable in a deployment topology.
+- Existing EF Core-backed data must be imported into vNext storage.
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: System MUST support versioned storage manifests contributed by modules.
+- **FR-002**: System MUST support provider-neutral storage units, fields, keys, and indexes.
+- **FR-003**: System MUST expose planner contracts so relational and document providers can consume the same manifest.
+- **FR-004**: System MUST expose provider capability validation before materialization.
+- **FR-005**: System MUST support provider-owned schema/index materialization.
+- **FR-006**: System MUST record applied schema versions per manifest.
+- **FR-007**: System MUST support idempotent startup materialization.
+- **FR-008**: System MUST support a portable document store for runtime-defined and module-defined documents.
+- **FR-009**: System MUST support portable queries only through declared indexes.
+- **FR-010**: System MUST return clear errors for unsupported or unindexed portable queries.
+- **FR-011**: System MUST support relational providers through document tables and index storage.
+- **FR-012**: System MUST support MongoDB through native collections and native indexes.
+- **FR-013**: System MUST support runtime-defined entity definitions with draft, published, and retired states.
+- **FR-014**: System MUST support provider-optimized physicalization policies as an opt-in.
+- **FR-015**: System MUST allow existing Elsa modules to migrate gradually from EF Core persistence.
+- **FR-016**: System MUST keep EF Core migrations out of the vNext schema authority path.
+- **FR-017**: System MUST expose diagnostics for manifest versions, provider status, and materialization failures.
+- **FR-018**: System MUST provide validation and tests for each supported provider.
+
+### Key Entities *(include if feature involves data)*
+
+- **Persistence Manifest**: Versioned module or runtime schema declaration.
+- **Storage Unit**: Provider-neutral persisted document/entity shape.
+- **Field**: Provider-neutral stored value declaration.
+- **Index**: Declared query path and uniqueness constraint.
+- **Provider Capability**: Provider-reported support for storage and query features.
+- **Document Envelope**: Stored document metadata and payload.
+- **Runtime Entity Definition**: Admin-defined entity manifest with lifecycle metadata.
+- **Schema History Entry**: Record of an applied manifest version.
+- **Physicalization Policy**: Storage optimization mode selected per storage unit or index.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: A module can declare persistence once and generate relational and document plans from the same manifest.
+- **SC-002**: SQLite materialization applies a manifest once and skips re-application on subsequent runs.
+- **SC-003**: A portable document store can save, load, delete, and query documents by declared indexes on SQLite.
+- **SC-004**: SQL Server and PostgreSQL providers pass the same portable document/index contract tests as SQLite.
+- **SC-005**: MongoDB provider passes the same document/index contract tests using native collections and indexes.
+- **SC-006**: Runtime-defined entities can be published and used without creating one physical table per entity by default.
+- **SC-007**: Unsupported provider capabilities are detected before materialization.
+- **SC-008**: At least one existing Elsa module runs on vNext persistence without EF Core migrations.
+- **SC-009**: Provider-specific migration projects are not required for the migrated vNext module.
+- **SC-010**: Hot-path physicalization can be enabled for at least one provider without changing module manifest code.
+
+## Assumptions
+
+- Runtime-defined entities use document storage by default.
+- Portable queries are index-driven and intentionally limited.
+- EF Core can remain a runtime query implementation where useful, but EF Core migrations are not the schema authority.
+- Provider-specific escape hatches are allowed when declared explicitly.
+- Workflow runtime hot paths are evaluated later and are not the first migration target.
+- Secrets is the preferred first real Elsa module migration candidate.
+- The reusable core should be extractable outside Elsa after validation against multiple providers.
diff --git a/specs/011-persistence-vnext/tasks.md b/specs/011-persistence-vnext/tasks.md
new file mode 100644
index 000000000..cae9b7e48
--- /dev/null
+++ b/specs/011-persistence-vnext/tasks.md
@@ -0,0 +1,79 @@
+# Persistence vNext Tasks
+
+## S1 - Core Manifest And Planner Foundations
+
+- [x] T001 Add provider-neutral storage descriptors.
+- [x] T002 Add relational planner and SQLite renderer.
+- [x] T003 Add SQL Server renderer.
+- [x] T004 Add document planner.
+- [x] T005 Add SQLite schema history/version runner.
+- [x] T006 Add Secrets sample manifest.
+- [x] T007 Add focused tests for relational, document, and version runner behavior.
+- [ ] T008 Open/update PR for S1.
+- [x] T009 Run self-review loop for S1.
+- [ ] T010 Merge S1 PR when no big issues remain.
+- [ ] T011 Close/complete associated S1 issue after merge.
+
+## S2 - Portable SQLite Document Store MVP
+
+- [x] T012 Define document envelope and store contracts.
+- [x] T013 Add SQLite document table materialization.
+- [x] T014 Implement save/load/delete.
+- [x] T015 Implement generic field index storage.
+- [x] T016 Implement index maintenance in the same transaction.
+- [x] T017 Implement query by declared indexes.
+- [x] T018 Add optimistic concurrency.
+- [x] T019 Add unindexed-query validation.
+- [x] T020 Add SQLite document store tests.
+- [ ] T021 Open/review/merge S2 PR and close associated issue.
+
+## S3 - SQL Server And PostgreSQL Relational Document Providers
+
+- [x] T022 Implement SQL Server document provider.
+- [x] T023 Implement PostgreSQL document provider.
+- [x] T024 Add startup locking/materialization strategy.
+- [ ] T025 Add relational provider contract tests.
+- [ ] T026 Open/review/merge S3 PR and close associated issue.
+
+## S4 - MongoDB Document Provider
+
+- [x] T027 Implement MongoDB document provider.
+- [x] T028 Materialize native collections and indexes.
+- [x] T029 Implement indexed query translation.
+- [x] T030 Add optimistic concurrency.
+- [ ] T031 Add MongoDB provider contract tests.
+- [ ] T032 Open/review/merge S4 PR and close associated issue.
+
+## S5 - Elsa Integration And First Module Migration
+
+- [x] T033 Add Elsa vNext persistence integration package.
+- [x] T034 Add shell feature/options/startup materialization.
+- [x] T035 Add diagnostics for applied manifests and provider status.
+- [x] T036 Implement Secrets vNext store.
+- [ ] T037 Add migration/import path from existing EF Core store if required.
+- [ ] T038 Open/review/merge S5 PR and close associated issue.
+
+## S6 - Runtime-Defined Entities
+
+- [x] T039 Define runtime entity definition model.
+- [x] T040 Add draft/published/retired lifecycle.
+- [x] T041 Add validation and capability checks.
+- [x] T042 Add CRUD/API integration.
+- [x] T043 Add audit trail.
+- [ ] T044 Open/review/merge S6 PR and close associated issue.
+
+## S7 - Physicalization And Performance
+
+- [x] T045 Add storage policy model.
+- [x] T046 Implement optimized relational indexes for one provider.
+- [x] T047 Implement native document-provider optimization for one provider.
+- [ ] T048 Add benchmarks.
+- [ ] T049 Open/review/merge S7 PR and close associated issue.
+
+## S8 - Workflow Runtime Evaluation And Hardening
+
+- [ ] T050 Benchmark runtime persistence candidates.
+- [ ] T051 Validate concurrency, locking, and retry behavior.
+- [x] T052 Add provider diagnostics and recovery guidance.
+- [x] T053 Document go/no-go decision for workflow runtime stores.
+- [ ] T054 Open/review/merge S8 PR and close associated issue.
diff --git a/src/modules/Elsa.AI.Abstractions/Contracts/IAIAuditEventHandler.cs b/src/modules/Elsa.AI.Abstractions/Contracts/IAIAuditEventHandler.cs
new file mode 100644
index 000000000..55a297022
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Contracts/IAIAuditEventHandler.cs
@@ -0,0 +1,14 @@
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Abstractions.Contracts;
+
+public interface IAIAuditEventHandler
+{
+ ValueTask RecordAsync(AIAuditEvent auditEvent, CancellationToken cancellationToken = default);
+
+ async ValueTask RecordManyAsync(IReadOnlyCollection auditEvents, CancellationToken cancellationToken = default)
+ {
+ foreach (var auditEvent in auditEvents)
+ await RecordAsync(auditEvent, cancellationToken);
+ }
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Contracts/IAIContextProvider.cs b/src/modules/Elsa.AI.Abstractions/Contracts/IAIContextProvider.cs
new file mode 100644
index 000000000..8ec1216db
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Contracts/IAIContextProvider.cs
@@ -0,0 +1,9 @@
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Abstractions.Contracts;
+
+public interface IAIContextProvider
+{
+ string Kind { get; }
+ ValueTask ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Contracts/IAIProposalStore.cs b/src/modules/Elsa.AI.Abstractions/Contracts/IAIProposalStore.cs
new file mode 100644
index 000000000..63055f944
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Contracts/IAIProposalStore.cs
@@ -0,0 +1,28 @@
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Abstractions.Contracts;
+
+public interface IAIConversationStore
+{
+ ValueTask FindAsync(string id, CancellationToken cancellationToken = default);
+ ValueTask SaveAsync(AIConversation conversation, CancellationToken cancellationToken = default);
+}
+
+public interface IAITransientConversationStore : IAIConversationStore;
+
+public interface IAIProposalStore
+{
+ ValueTask FindAsync(string id, string? tenantId, CancellationToken cancellationToken = default);
+ ValueTask SaveAsync(AIProposal proposal, CancellationToken cancellationToken = default);
+}
+
+public interface IAIAuditSink
+{
+ ValueTask RecordAsync(AIAuditEvent auditEvent, CancellationToken cancellationToken = default);
+
+ async ValueTask RecordManyAsync(IReadOnlyCollection auditEvents, CancellationToken cancellationToken = default)
+ {
+ foreach (var auditEvent in auditEvents)
+ await RecordAsync(auditEvent, cancellationToken);
+ }
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Contracts/IAIProvider.cs b/src/modules/Elsa.AI.Abstractions/Contracts/IAIProvider.cs
new file mode 100644
index 000000000..591aa2f92
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Contracts/IAIProvider.cs
@@ -0,0 +1,15 @@
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Abstractions.Contracts;
+
+public interface IAIProvider
+{
+ string Name { get; }
+ ValueTask CreateSessionAsync(CreateAISessionRequest request, CancellationToken cancellationToken = default);
+ IAsyncEnumerable ExecuteTurnAsync(AITurnRequest request, CancellationToken cancellationToken = default);
+}
+
+public interface IAIOrchestrator
+{
+ IAsyncEnumerable ExecuteChatAsync(AIChatRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Contracts/IAITool.cs b/src/modules/Elsa.AI.Abstractions/Contracts/IAITool.cs
new file mode 100644
index 000000000..3606acede
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Contracts/IAITool.cs
@@ -0,0 +1,15 @@
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Abstractions.Contracts;
+
+public interface IAITool : IDisposable
+{
+ AIToolDefinition Definition { get; }
+ ValueTask ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellationToken = default);
+}
+
+public interface IAIToolRegistry
+{
+ ValueTask> ListAsync(AIToolQuery query, CancellationToken cancellationToken = default);
+ ValueTask FindAsync(string name, AIToolQuery query, CancellationToken cancellationToken = default);
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Elsa.AI.Abstractions.csproj b/src/modules/Elsa.AI.Abstractions/Elsa.AI.Abstractions.csproj
new file mode 100644
index 000000000..650acbe3b
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Elsa.AI.Abstractions.csproj
@@ -0,0 +1,10 @@
+
+
+
+
+ Provides provider-neutral abstractions for Elsa AI capabilities.
+
+ elsa module ai abstractions
+
+
+
diff --git a/src/modules/Elsa.AI.Abstractions/FodyWeavers.xml b/src/modules/Elsa.AI.Abstractions/FodyWeavers.xml
new file mode 100644
index 000000000..00e1d9a1c
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/modules/Elsa.AI.Abstractions/Models/AIAuditEvent.cs b/src/modules/Elsa.AI.Abstractions/Models/AIAuditEvent.cs
new file mode 100644
index 000000000..df3d807a2
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Models/AIAuditEvent.cs
@@ -0,0 +1,16 @@
+namespace Elsa.AI.Abstractions.Models;
+
+public record AIAuditEvent
+{
+ public string Id { get; init; } = Guid.NewGuid().ToString("N");
+ public string? TenantId { get; init; }
+ public string ActorId { get; init; } = "";
+ public string? ConversationId { get; init; }
+ public string? ProposalId { get; init; }
+ public string? ToolInvocationId { get; init; }
+ public string Type { get; init; } = "";
+ public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
+ public string? TraceId { get; init; }
+ public string Summary { get; init; } = "";
+ public JsonObject Data { get; init; } = [];
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Models/AIContextAttachment.cs b/src/modules/Elsa.AI.Abstractions/Models/AIContextAttachment.cs
new file mode 100644
index 000000000..de8970499
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Models/AIContextAttachment.cs
@@ -0,0 +1,36 @@
+namespace Elsa.AI.Abstractions.Models;
+
+public record AIContextAttachment
+{
+ public string? Id { get; init; }
+ public string? ConversationId { get; init; }
+ public string Kind { get; init; } = default!;
+ public string? ReferenceId { get; init; }
+ public string? TenantId { get; init; }
+ public string? Scope { get; init; }
+ public AITimeRange? TimeRange { get; init; }
+ public string? ActivityId { get; init; }
+ public JsonObject Metadata { get; init; } = [];
+}
+
+public record AIResolvedContext
+{
+ public string Kind { get; init; } = default!;
+ public string? ReferenceId { get; init; }
+ public string Summary { get; init; } = "";
+ public JsonObject Data { get; init; } = [];
+ public JsonObject Metadata { get; init; } = [];
+}
+
+public record AIContextResolutionRequest
+{
+ public AIContextAttachment Attachment { get; init; } = default!;
+ public string? TenantId { get; init; }
+ public string UserId { get; init; } = default!;
+}
+
+public record AITimeRange
+{
+ public DateTimeOffset From { get; init; }
+ public DateTimeOffset To { get; init; }
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Models/AIConversation.cs b/src/modules/Elsa.AI.Abstractions/Models/AIConversation.cs
new file mode 100644
index 000000000..c90f5ccaf
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Models/AIConversation.cs
@@ -0,0 +1,124 @@
+namespace Elsa.AI.Abstractions.Models;
+
+public record AIConversation
+{
+ public string Id { get; init; } = default!;
+ public string? TenantId { get; init; }
+ public string UserId { get; init; } = default!;
+ public string? Title { get; init; }
+ public AIConversationStatus Status { get; init; } = AIConversationStatus.Active;
+ public DateTimeOffset CreatedAt { get; init; }
+ public DateTimeOffset UpdatedAt { get; init; }
+ public string? ProviderSessionId { get; init; }
+ public AIRetentionMode RetentionMode { get; init; } = AIRetentionMode.Configured;
+ public DateTimeOffset? RetentionExpiresAt { get; init; }
+ public IReadOnlyCollection Messages { get; init; } = [];
+}
+
+public record AIMessage
+{
+ public string Id { get; init; } = default!;
+ public string ConversationId { get; init; } = default!;
+ public AIMessageRole Role { get; init; }
+ public string Content { get; init; } = "";
+ public DateTimeOffset CreatedAt { get; init; }
+ public long StreamSequence { get; init; }
+ public JsonObject Metadata { get; init; } = [];
+}
+
+public record AIChatRequest
+{
+ public string? ConversationId { get; init; }
+ public string Message { get; init; } = "";
+ public string? Agent { get; init; }
+ public string? ProviderName { get; init; }
+ public ICollection Attachments { get; init; } = [];
+ public string? TenantId { get; init; }
+ public string UserId { get; init; } = default!;
+ public ICollection UserPermissions { get; init; } = [];
+ public bool IsReconnect { get; init; }
+}
+
+public record AIStreamEvent
+{
+ public string Type { get; init; } = default!;
+ public string ConversationId { get; init; } = default!;
+ public long Sequence { get; init; }
+ public DateTimeOffset Timestamp { get; init; }
+ public JsonObject Data { get; init; } = [];
+}
+
+public record AISessionHandle
+{
+ public string Id { get; init; } = Guid.NewGuid().ToString("N");
+ public string? ProviderSessionId { get; init; }
+}
+
+public record CreateAISessionRequest
+{
+ public string ConversationId { get; init; } = default!;
+ public string? Agent { get; init; }
+ public string? TenantId { get; init; }
+ public AIProviderConfiguration? ProviderConfiguration { get; init; }
+ public JsonObject Metadata { get; init; } = [];
+}
+
+public record AITurnRequest
+{
+ public string ConversationId { get; init; } = default!;
+ public string? ProviderSessionId { get; init; }
+ public string Message { get; init; } = "";
+ public IReadOnlyCollection Messages { get; init; } = [];
+ public IReadOnlyCollection Context { get; init; } = [];
+ public IReadOnlyCollection Tools { get; init; } = [];
+ public IReadOnlyCollection ToolResults { get; init; } = [];
+ public string? Agent { get; init; }
+ public AIProviderConfiguration? ProviderConfiguration { get; init; }
+}
+
+public record AIProviderConfiguration
+{
+ public string Name { get; init; } = default!;
+ public string Provider { get; init; } = default!;
+ public string? Model { get; init; }
+ public string? ApiKeySecretName { get; init; }
+ public string? Endpoint { get; init; }
+}
+
+public record AIToolTurnResult
+{
+ public string ToolCallId { get; init; } = default!;
+ public string ToolName { get; init; } = default!;
+ public AIToolResult Result { get; init; } = new();
+}
+
+public record AIProviderEvent
+{
+ public string Type { get; init; } = default!;
+ public long Sequence { get; init; }
+ public DateTimeOffset Timestamp { get; init; }
+ public JsonObject Data { get; init; } = [];
+}
+
+public enum AIConversationStatus
+{
+ Active,
+ Completed,
+ Failed,
+ Expired
+}
+
+public enum AIRetentionMode
+{
+ Configured,
+ Ephemeral,
+ Durable
+}
+
+public enum AIMessageRole
+{
+ User,
+ Assistant,
+ System,
+ Tool
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Models/AIProposal.cs b/src/modules/Elsa.AI.Abstractions/Models/AIProposal.cs
new file mode 100644
index 000000000..cc0ab94d4
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Models/AIProposal.cs
@@ -0,0 +1,63 @@
+namespace Elsa.AI.Abstractions.Models;
+
+public record AIProposal
+{
+ public string Id { get; init; } = Guid.NewGuid().ToString("N");
+ public string? TenantId { get; init; }
+ public string ConversationId { get; init; } = "";
+ public AIProposalKind Kind { get; init; }
+ public AIProposalStatus Status { get; init; } = AIProposalStatus.Draft;
+ public string? BaselineWorkflowDefinitionId { get; init; }
+ public string? BaselineVersionId { get; init; }
+ public JsonObject WorkflowPayload { get; init; } = [];
+ public string Rationale { get; init; } = "";
+ public ICollection Warnings { get; init; } = [];
+ public ICollection ValidationDiagnostics { get; init; } = [];
+ public AIGraphDiff? GraphDiff { get; init; }
+ public string CreatedBy { get; init; } = "";
+ public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
+ public string? ReviewedBy { get; init; }
+ public DateTimeOffset? ReviewedAt { get; init; }
+ public string? AppliedBy { get; init; }
+ public DateTimeOffset? AppliedAt { get; init; }
+}
+
+public record AIValidationDiagnostic
+{
+ public string Code { get; init; } = default!;
+ public string Message { get; init; } = "";
+ public AIValidationSeverity Severity { get; init; } = AIValidationSeverity.Warning;
+ public string? Path { get; init; }
+}
+
+public record AIGraphDiff
+{
+ public ICollection AddedActivityIds { get; init; } = [];
+ public ICollection RemovedActivityIds { get; init; } = [];
+ public ICollection ChangedActivityIds { get; init; } = [];
+ public JsonObject Data { get; init; } = [];
+}
+
+public enum AIProposalKind
+{
+ WorkflowCreate,
+ WorkflowUpdate
+}
+
+public enum AIProposalStatus
+{
+ Draft,
+ Validated,
+ Blocked,
+ Approved,
+ Rejected,
+ Applied,
+ Expired
+}
+
+public enum AIValidationSeverity
+{
+ Information,
+ Warning,
+ Error
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Models/AIToolDefinition.cs b/src/modules/Elsa.AI.Abstractions/Models/AIToolDefinition.cs
new file mode 100644
index 000000000..0905e66d8
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Models/AIToolDefinition.cs
@@ -0,0 +1,109 @@
+namespace Elsa.AI.Abstractions.Models;
+
+public record AIToolDefinition
+{
+ public string Name { get; init; } = "";
+ public string DisplayName { get; init; } = "";
+ public string Description { get; init; } = "";
+ public JsonObject Schema { get; init; } = [];
+ public AIToolMutability Mutability { get; init; } = AIToolMutability.ReadOnly;
+ public AIToolDangerLevel DangerLevel { get; init; } = AIToolDangerLevel.Low;
+ public ICollection Permissions { get; init; } = [];
+ public AITenantBehavior TenantBehavior { get; init; } = AITenantBehavior.TenantScoped;
+ public AIToolAuditBehavior AuditBehavior { get; init; } = AIToolAuditBehavior.RecordInvocation;
+ public ICollection AgentScopes { get; init; } = [];
+ public ICollection TenantIds { get; init; } = [];
+ public ICollection ActorIds { get; init; } = [];
+ public string? Provider { get; init; }
+ public bool EnabledByDefault { get; init; }
+ public bool IsEnabled { get; init; }
+}
+
+public record AIToolInvocation
+{
+ public string Id { get; init; } = default!;
+ public string ConversationId { get; init; } = default!;
+ public string ToolName { get; init; } = default!;
+ public JsonObject Arguments { get; init; } = [];
+ public AIToolAuthorizationResult AuthorizationResult { get; init; } = AIToolAuthorizationResult.NotEvaluated;
+ public DateTimeOffset StartedAt { get; init; }
+ public DateTimeOffset? CompletedAt { get; init; }
+ public AIToolInvocationStatus Status { get; init; } = AIToolInvocationStatus.Pending;
+ public string? ResultSummary { get; init; }
+ public string? Error { get; init; }
+ public string? TraceId { get; init; }
+ public string? TenantId { get; init; }
+ public string ActorId { get; init; } = default!;
+}
+
+public record AIToolResult
+{
+ public AIToolInvocationStatus Status { get; init; } = AIToolInvocationStatus.Completed;
+ public string Summary { get; init; } = "";
+ public JsonObject Data { get; init; } = [];
+ public string? Error { get; init; }
+}
+
+public record AIToolExecutionContext
+{
+ public string ConversationId { get; init; } = default!;
+ public string? TenantId { get; init; }
+ public string ActorId { get; init; } = default!;
+ public string? Agent { get; init; }
+ public JsonObject Arguments { get; init; } = [];
+}
+
+public record AIToolQuery
+{
+ public string? Agent { get; init; }
+ public AIToolMutability? Mutability { get; init; }
+ public AIToolDangerLevel? DangerLevel { get; init; }
+ public string? TenantId { get; init; }
+ public string? ActorId { get; init; }
+ public ICollection UserPermissions { get; init; } = [];
+}
+
+public enum AIToolMutability
+{
+ ReadOnly,
+ Proposal,
+ Administrative
+}
+
+public enum AIToolDangerLevel
+{
+ Low,
+ Medium,
+ High,
+ Critical
+}
+
+public enum AITenantBehavior
+{
+ TenantScoped,
+ HostScoped,
+ CrossTenantDenied
+}
+
+public enum AIToolAuditBehavior
+{
+ None,
+ RecordInvocation,
+ RecordInvocationAndResult
+}
+
+public enum AIToolAuthorizationResult
+{
+ NotEvaluated,
+ Allowed,
+ Denied
+}
+
+public enum AIToolInvocationStatus
+{
+ Pending,
+ Running,
+ Completed,
+ Failed,
+ Denied
+}
diff --git a/src/modules/Elsa.AI.Abstractions/Usings.cs b/src/modules/Elsa.AI.Abstractions/Usings.cs
new file mode 100644
index 000000000..b3fb2b505
--- /dev/null
+++ b/src/modules/Elsa.AI.Abstractions/Usings.cs
@@ -0,0 +1 @@
+global using System.Text.Json.Nodes;
diff --git a/src/modules/Elsa.AI.Copilot/Adapters/CopilotProvider.cs b/src/modules/Elsa.AI.Copilot/Adapters/CopilotProvider.cs
new file mode 100644
index 000000000..9f7a08eee
--- /dev/null
+++ b/src/modules/Elsa.AI.Copilot/Adapters/CopilotProvider.cs
@@ -0,0 +1,38 @@
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+using Elsa.AI.Copilot.Options;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.AI.Copilot.Adapters;
+
+public class CopilotProvider(IOptions options) : IAIProvider
+{
+ public string Name => options.Value.ProviderName ?? "copilot";
+
+ public ValueTask CreateSessionAsync(CreateAISessionRequest request, CancellationToken cancellationToken = default)
+ {
+ var providerName = request.ProviderConfiguration?.Name ?? options.Value.ProviderName ?? "copilot";
+
+ return ValueTask.FromResult(new AISessionHandle
+ {
+ Id = request.ConversationId,
+ ProviderSessionId = $"{providerName}:{request.ConversationId}"
+ });
+ }
+
+ public async IAsyncEnumerable ExecuteTurnAsync(AITurnRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await Task.Yield();
+
+ yield return new AIProviderEvent
+ {
+ Type = "assistant.delta",
+ Sequence = 1,
+ Timestamp = DateTimeOffset.UtcNow,
+ Data = new JsonObject
+ {
+ ["content"] = "Copilot adapter is registered. Runtime CLI integration is deferred to the provider implementation slice."
+ }
+ };
+ }
+}
diff --git a/src/modules/Elsa.AI.Copilot/Elsa.AI.Copilot.csproj b/src/modules/Elsa.AI.Copilot/Elsa.AI.Copilot.csproj
new file mode 100644
index 000000000..80f9b75bf
--- /dev/null
+++ b/src/modules/Elsa.AI.Copilot/Elsa.AI.Copilot.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+ Adapts the GitHub Copilot SDK and headless CLI to Elsa AI provider contracts.
+
+ elsa module ai copilot
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Elsa.AI.Copilot/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.AI.Copilot/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..003628dad
--- /dev/null
+++ b/src/modules/Elsa.AI.Copilot/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,22 @@
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Copilot.Adapters;
+using Elsa.AI.Copilot.Options;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddCopilotAIProvider(this IServiceCollection services, Action? configureOptions = null)
+ {
+ if (configureOptions != null)
+ services.Configure(configureOptions);
+
+ services.AddOptions();
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
+
+ return services;
+ }
+}
diff --git a/src/modules/Elsa.AI.Copilot/FodyWeavers.xml b/src/modules/Elsa.AI.Copilot/FodyWeavers.xml
new file mode 100644
index 000000000..dfa2f2e5f
--- /dev/null
+++ b/src/modules/Elsa.AI.Copilot/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/modules/Elsa.AI.Copilot/Options/CopilotOptions.cs b/src/modules/Elsa.AI.Copilot/Options/CopilotOptions.cs
new file mode 100644
index 000000000..ec97d6de7
--- /dev/null
+++ b/src/modules/Elsa.AI.Copilot/Options/CopilotOptions.cs
@@ -0,0 +1,8 @@
+namespace Elsa.AI.Copilot.Options;
+
+public class CopilotOptions
+{
+ public string CliPath { get; set; } = "copilot";
+ public string? Model { get; set; }
+ public string? ProviderName { get; set; } = "copilot";
+}
diff --git a/src/modules/Elsa.AI.Copilot/ShellFeatures/CopilotAIFeature.cs b/src/modules/Elsa.AI.Copilot/ShellFeatures/CopilotAIFeature.cs
new file mode 100644
index 000000000..5eca65ea6
--- /dev/null
+++ b/src/modules/Elsa.AI.Copilot/ShellFeatures/CopilotAIFeature.cs
@@ -0,0 +1,33 @@
+using CShells.Features;
+using Elsa.AI.Copilot.Options;
+using Elsa.Extensions;
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.AI.Copilot.ShellFeatures;
+
+[ShellFeature(
+ "CopilotAI",
+ DisplayName = "Copilot AI Provider",
+ Description = "Registers the Copilot adapter for Weaver AI")]
+[UsedImplicitly]
+public class CopilotAIFeature : IShellFeature
+{
+ private static readonly CopilotOptions DefaultOptions = new();
+
+ public string CliPath { get; set; } = DefaultOptions.CliPath;
+ public string? Model { get; set; } = DefaultOptions.Model;
+ public string? ProviderName { get; set; } = DefaultOptions.ProviderName;
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddCopilotAIProvider(ConfigureOptions);
+ }
+
+ private void ConfigureOptions(CopilotOptions options)
+ {
+ options.CliPath = CliPath;
+ options.Model = Model;
+ options.ProviderName = ProviderName;
+ }
+}
diff --git a/src/modules/Elsa.AI.Copilot/Usings.cs b/src/modules/Elsa.AI.Copilot/Usings.cs
new file mode 100644
index 000000000..b3fb2b505
--- /dev/null
+++ b/src/modules/Elsa.AI.Copilot/Usings.cs
@@ -0,0 +1 @@
+global using System.Text.Json.Nodes;
diff --git a/src/modules/Elsa.AI.Host/Context/AIContextResolver.cs b/src/modules/Elsa.AI.Host/Context/AIContextResolver.cs
new file mode 100644
index 000000000..9390cc933
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Context/AIContextResolver.cs
@@ -0,0 +1,104 @@
+using System.Text.RegularExpressions;
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Elsa.AI.Host.Context;
+
+public class AIContextResolver(IServiceScopeFactory scopeFactory, ILogger logger)
+{
+ private static readonly string[] SensitiveKeyFragments = ["secret", "token", "password", "apikey", "api_key", "api-key", "authorization", "credential", "bearer"];
+ private static readonly Regex[] SensitiveValuePatterns =
+ [
+ new(@"\bBearer\s+[A-Za-z0-9._~+/\-=]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled),
+ new(@"\b(?:api[_-]?key|token|secret|password)\s*[:=]\s*['""]?[A-Za-z0-9._~+/\-=]{8,}['""]?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled)
+ ];
+ private const string Redacted = "[redacted]";
+
+ public async ValueTask> ResolveAsync(AIChatRequest request, CancellationToken cancellationToken = default)
+ {
+ using var scope = scopeFactory.CreateScope();
+ var providers = BuildProviders(scope.ServiceProvider.GetServices());
+ var resolved = new List();
+ var attachmentsWithProviders = request.Attachments
+ .Select(attachment => new
+ {
+ Attachment = attachment,
+ Provider = providers.GetValueOrDefault(attachment.Kind)
+ })
+ .Where(x => x.Provider != null);
+
+ foreach (var item in attachmentsWithProviders)
+ {
+ if (item.Provider is IPlaceholderAIContextProvider)
+ logger.LogWarning(
+ "AI context kind {ContextKind} is using placeholder provider {ProviderType}. Replace it before production use.",
+ item.Attachment.Kind,
+ item.Provider.GetType().Name);
+
+ var context = await item.Provider!.ResolveAsync(new AIContextResolutionRequest
+ {
+ Attachment = item.Attachment,
+ TenantId = request.TenantId,
+ UserId = request.UserId
+ }, cancellationToken);
+
+ resolved.Add(Redact(context));
+ }
+
+ return resolved;
+ }
+
+ private static AIResolvedContext Redact(AIResolvedContext context) =>
+ context with
+ {
+ Summary = RedactText(context.Summary),
+ Data = RedactObject(context.Data),
+ Metadata = RedactObject(context.Metadata)
+ };
+
+ private static JsonObject RedactObject(JsonObject source)
+ {
+ var redacted = new JsonObject();
+
+ foreach (var property in source)
+ redacted[property.Key] = IsSensitiveKey(property.Key) ? Redacted : RedactNode(property.Value);
+
+ return redacted;
+ }
+
+ private static JsonNode? RedactNode(JsonNode? node) =>
+ node switch
+ {
+ JsonObject jsonObject => RedactObject(jsonObject),
+ JsonArray jsonArray => new JsonArray(jsonArray.Select(RedactNode).ToArray()),
+ JsonValue jsonValue when jsonValue.TryGetValue(out var value) => JsonValue.Create(RedactText(value)),
+ JsonValue jsonValue => jsonValue.DeepClone(),
+ _ => node?.DeepClone()
+ };
+
+ private static string RedactText(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ return text;
+
+ return SensitiveValuePatterns.Aggregate(text, (current, pattern) => pattern.Replace(current, Redacted));
+ }
+
+ private static bool IsSensitiveKey(string key) =>
+ SensitiveKeyFragments.Any(fragment => key.Contains(fragment, StringComparison.OrdinalIgnoreCase));
+
+ private static Dictionary BuildProviders(IEnumerable providers)
+ {
+ return providers
+ .GroupBy(x => x.Kind, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(x => x.Key, SelectProvider, StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static IAIContextProvider SelectProvider(IEnumerable providers)
+ {
+ var providerList = providers.ToList();
+ return providerList.LastOrDefault(x => x is not IPlaceholderAIContextProvider) ?? providerList.Last();
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Context/IPlaceholderAIContextProvider.cs b/src/modules/Elsa.AI.Host/Context/IPlaceholderAIContextProvider.cs
new file mode 100644
index 000000000..962061a10
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Context/IPlaceholderAIContextProvider.cs
@@ -0,0 +1,5 @@
+using Elsa.AI.Abstractions.Contracts;
+
+namespace Elsa.AI.Host.Context;
+
+public interface IPlaceholderAIContextProvider : IAIContextProvider;
diff --git a/src/modules/Elsa.AI.Host/Context/WorkflowDefinitionContextProvider.cs b/src/modules/Elsa.AI.Host/Context/WorkflowDefinitionContextProvider.cs
new file mode 100644
index 000000000..52081028b
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Context/WorkflowDefinitionContextProvider.cs
@@ -0,0 +1,29 @@
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Host.Context;
+
+///
+/// Placeholder workflow definition context provider. Replace this implementation with a workflow-management-backed resolver before using workflow context for production AI reasoning.
+///
+public class WorkflowDefinitionContextProvider : IAIContextProvider, IPlaceholderAIContextProvider
+{
+ public string Kind => "WorkflowDefinition";
+
+ public ValueTask ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default)
+ {
+ var attachment = request.Attachment;
+
+ return ValueTask.FromResult(new AIResolvedContext
+ {
+ Kind = Kind,
+ ReferenceId = attachment.ReferenceId,
+ Summary = $"Workflow definition context provider is not implemented; only reference {attachment.ReferenceId} was received.",
+ Data = [],
+ Metadata = new JsonObject
+ {
+ ["activityId"] = attachment.ActivityId
+ }
+ });
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Context/WorkflowInstanceContextProvider.cs b/src/modules/Elsa.AI.Host/Context/WorkflowInstanceContextProvider.cs
new file mode 100644
index 000000000..78811563d
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Context/WorkflowInstanceContextProvider.cs
@@ -0,0 +1,29 @@
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Host.Context;
+
+///
+/// Placeholder workflow instance context provider. Replace this implementation with a runtime-backed resolver before using instance context for production AI reasoning.
+///
+public class WorkflowInstanceContextProvider : IAIContextProvider, IPlaceholderAIContextProvider
+{
+ public string Kind => "WorkflowInstance";
+
+ public ValueTask ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default)
+ {
+ var attachment = request.Attachment;
+
+ return ValueTask.FromResult(new AIResolvedContext
+ {
+ Kind = Kind,
+ ReferenceId = attachment.ReferenceId,
+ Summary = $"Workflow instance context provider is not implemented; only reference {attachment.ReferenceId} was received.",
+ Data = [],
+ Metadata = new JsonObject
+ {
+ ["activityId"] = attachment.ActivityId
+ }
+ });
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Elsa.AI.Host.csproj b/src/modules/Elsa.AI.Host/Elsa.AI.Host.csproj
new file mode 100644
index 000000000..b0af6f924
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Elsa.AI.Host.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+ Hosts Elsa AI orchestration, tools, context resolution, proposals, and audit services.
+
+ elsa module ai host
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Elsa.AI.Host/Endpoints/AI/AIHttpContextIdentity.cs b/src/modules/Elsa.AI.Host/Endpoints/AI/AIHttpContextIdentity.cs
new file mode 100644
index 000000000..916736fec
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Endpoints/AI/AIHttpContextIdentity.cs
@@ -0,0 +1,61 @@
+using System.Security.Claims;
+using Elsa.AI.Host.Options;
+using Elsa.Common.Multitenancy;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.AI.Host.Endpoints.AI;
+
+internal static class AIHttpContextIdentity
+{
+ private const string TenantIdClaimType = "http://schemas.microsoft.com/identity/claims/tenantid";
+
+ public static string GetActorId(HttpContext? context) =>
+ context?.User.FindFirstValue(ClaimTypes.NameIdentifier) ??
+ context?.User.FindFirstValue("sub") ??
+ context?.User.Identity?.Name ??
+ "anonymous";
+
+ public static string? GetTenantId(HttpContext? context)
+ {
+ var tenantAccessor = context?.RequestServices?.GetService();
+ if (tenantAccessor != null)
+ return tenantAccessor.TenantId;
+
+ return context?.User.FindFirstValue(TenantIdClaimType) ??
+ context?.User.FindFirstValue("tenant_id") ??
+ context?.User.FindFirstValue("tenantId");
+ }
+
+ public static ICollection GetPermissions(HttpContext? context) =>
+ context?.User
+ .FindAll(PermissionNames.ClaimType)
+ .Select(x => x.Value)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList() ?? [];
+
+ public static string? GetAuthorizedAgent(string? requestedAgent, AIHostOptions options, ICollection userPermissions)
+ {
+ if (string.IsNullOrWhiteSpace(requestedAgent))
+ return null;
+
+ var agent = options.Agents.FirstOrDefault(x => string.Equals(x.Name, requestedAgent, StringComparison.OrdinalIgnoreCase));
+ if (agent == null || !HasRequiredPermissions(agent.Permissions, userPermissions))
+ return null;
+
+ return agent.Name;
+ }
+
+ private static bool HasRequiredPermissions(ICollection requiredPermissions, ICollection userPermissions)
+ {
+ if (requiredPermissions.Count == 0)
+ return true;
+
+ var grantedPermissions = userPermissions
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ return grantedPermissions.Contains(PermissionNames.All) || requiredPermissions.All(grantedPermissions.Contains);
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Endpoints/AI/Capabilities/Endpoint.cs b/src/modules/Elsa.AI.Host/Endpoints/AI/Capabilities/Endpoint.cs
new file mode 100644
index 000000000..541fd5d7b
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Endpoints/AI/Capabilities/Endpoint.cs
@@ -0,0 +1,62 @@
+using Elsa.Abstractions;
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Host.Options;
+using Elsa.AI.Host.Permissions;
+using JetBrains.Annotations;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.AI.Host.Endpoints.AI.Capabilities;
+
+[PublicAPI]
+public class Endpoint(
+ IOptions options,
+ IEnumerable providers,
+ IEnumerable conversationStores,
+ IEnumerable proposalStores) : ElsaEndpointWithoutRequest
+{
+ public override void Configure()
+ {
+ Get("/ai/capabilities");
+ ConfigurePermissions(AIPermissions.ViewCapabilities);
+ }
+
+ public override Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ var optionsValue = options.Value;
+ var providerOptions = optionsValue.Providers.ToList();
+ var availableProviders = providers.Where(x => providerOptions.IsProviderEnabled(x.Name)).ToList();
+ var hasSelectableProvider = HasSelectableProvider(optionsValue.DefaultProviderName, providerOptions, availableProviders);
+ var hasDurableConversationStore = conversationStores.Any(x => x is not IAITransientConversationStore);
+
+ return Task.FromResult(new Response(
+ optionsValue.StreamingEnabled && hasSelectableProvider,
+ optionsValue.ConversationPersistenceEnabled && hasDurableConversationStore,
+ optionsValue.ProposalReviewEnabled && proposalStores.Any(),
+ optionsValue.SupportedAttachmentKinds.ToList(),
+ optionsValue.Agents.Select(x => new AIAgentCapability(x.Name, x.DisplayName, x.Description)).ToList()));
+ }
+
+ private static bool HasSelectableProvider(string? providerName, IReadOnlyCollection providerOptions, IReadOnlyCollection availableProviders)
+ {
+ if (!string.IsNullOrWhiteSpace(providerName))
+ {
+ var configuredProviders = providerOptions.Where(x => x.Enabled).ToList();
+ var configuredProvider = configuredProviders.FirstOrDefault(x => string.Equals(x.Name, providerName, StringComparison.OrdinalIgnoreCase));
+ return configuredProvider != null
+ ? availableProviders.Any(x => string.Equals(x.Name, configuredProvider.Name, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(x.Name, configuredProvider.Provider, StringComparison.OrdinalIgnoreCase))
+ : availableProviders.Any(x => string.Equals(x.Name, providerName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ return availableProviders.Count == 1;
+ }
+}
+
+public record Response(
+ bool Streaming,
+ bool ConversationPersistence,
+ bool ProposalReview,
+ IReadOnlyCollection SupportedAttachmentKinds,
+ IReadOnlyCollection Agents);
+
+public record AIAgentCapability(string Name, string DisplayName, string Description);
diff --git a/src/modules/Elsa.AI.Host/Endpoints/AI/Chat/Endpoint.cs b/src/modules/Elsa.AI.Host/Endpoints/AI/Chat/Endpoint.cs
new file mode 100644
index 000000000..69bf76cb1
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Endpoints/AI/Chat/Endpoint.cs
@@ -0,0 +1,90 @@
+using System.Text.Json;
+using Elsa.Abstractions;
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+using Elsa.AI.Host.Endpoints.AI;
+using Elsa.AI.Host.Options;
+using Elsa.AI.Host.Permissions;
+using Elsa.AI.Host.Streaming;
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.AI.Host.Endpoints.AI.Chat;
+
+[PublicAPI]
+public class Endpoint(
+ IAIOrchestrator orchestrator,
+ AIStreamSessionManager sessionManager,
+ IOptions options) : ElsaEndpoint
+{
+ public override void Configure()
+ {
+ Post("/ai/chat");
+ ConfigurePermissions(AIPermissions.Chat);
+ }
+
+ public override async Task HandleAsync(AIChatRequest request, CancellationToken cancellationToken)
+ {
+ var conversationId = string.IsNullOrWhiteSpace(request.ConversationId) ? Guid.NewGuid().ToString("N") : request.ConversationId;
+ var userPermissions = AIHttpContextIdentity.GetPermissions(HttpContext);
+ request = request with
+ {
+ ConversationId = conversationId,
+ Message = request.Message ?? "",
+ Attachments = request.Attachments ?? [],
+ IsReconnect = sessionManager.CanReconnect(conversationId),
+ TenantId = AIHttpContextIdentity.GetTenantId(HttpContext),
+ UserId = AIHttpContextIdentity.GetActorId(HttpContext),
+ UserPermissions = userPermissions,
+ Agent = AIHttpContextIdentity.GetAuthorizedAgent(request.Agent, options.Value, userPermissions),
+ ProviderName = null
+ };
+ var response = HttpContext.Response;
+ response.ContentType = "text/event-stream";
+ response.Headers["Cache-Control"] = "no-cache";
+
+ var completed = false;
+ var reconnectAccepted = request.IsReconnect;
+ var requestedReconnectConversationId = request.ConversationId;
+ var reconnectConnected = false;
+ var disconnectedConversationId = request.ConversationId;
+ try
+ {
+ await foreach (var streamEvent in orchestrator.ExecuteChatAsync(request, cancellationToken))
+ {
+ disconnectedConversationId = streamEvent.ConversationId;
+ if (reconnectAccepted && !reconnectConnected)
+ {
+ if (!string.Equals(requestedReconnectConversationId, disconnectedConversationId, StringComparison.OrdinalIgnoreCase))
+ sessionManager.ReleaseReconnect(requestedReconnectConversationId);
+
+ sessionManager.MarkConnected(disconnectedConversationId);
+ reconnectConnected = true;
+ }
+
+ await response.WriteAsync($"event: {streamEvent.Type}\n", cancellationToken);
+ await response.WriteAsync($"data: {JsonSerializer.Serialize(streamEvent)}\n\n", cancellationToken);
+ await response.Body.FlushAsync(cancellationToken);
+ }
+
+ completed = true;
+ }
+ catch (OperationCanceledException) when (HttpContext.RequestAborted.IsCancellationRequested)
+ {
+ // Expected when the client disconnects; the finally block records reconnect state.
+ return;
+ }
+ finally
+ {
+ if (!completed)
+ {
+ sessionManager.MarkDisconnected(disconnectedConversationId, options.Value.ReconnectGrace);
+ }
+ else if (reconnectAccepted && !reconnectConnected)
+ {
+ sessionManager.ReleaseReconnect(requestedReconnectConversationId);
+ }
+ }
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Endpoints/AI/Tools/Endpoint.cs b/src/modules/Elsa.AI.Host/Endpoints/AI/Tools/Endpoint.cs
new file mode 100644
index 000000000..9dad64be8
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Endpoints/AI/Tools/Endpoint.cs
@@ -0,0 +1,37 @@
+using Elsa.Abstractions;
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+using Elsa.AI.Host.Endpoints.AI;
+using Elsa.AI.Host.Options;
+using Elsa.AI.Host.Permissions;
+using JetBrains.Annotations;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.AI.Host.Endpoints.AI.Tools;
+
+[PublicAPI]
+public class Endpoint(IAIToolRegistry toolRegistry, IOptions options) : ElsaEndpoint>
+{
+ public override void Configure()
+ {
+ Get("/ai/tools");
+ ConfigurePermissions(AIPermissions.ViewTools);
+ }
+
+ public override async Task> ExecuteAsync(Request request, CancellationToken cancellationToken)
+ {
+ var userPermissions = AIHttpContextIdentity.GetPermissions(HttpContext);
+ return await toolRegistry.ListAsync(new AIToolQuery
+ {
+ Agent = AIHttpContextIdentity.GetAuthorizedAgent(request.Agent, options.Value, userPermissions),
+ ActorId = AIHttpContextIdentity.GetActorId(HttpContext),
+ TenantId = AIHttpContextIdentity.GetTenantId(HttpContext),
+ UserPermissions = userPermissions
+ }, cancellationToken);
+ }
+}
+
+public class Request
+{
+ public string? Agent { get; set; }
+}
diff --git a/src/modules/Elsa.AI.Host/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.AI.Host/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..95027cfc8
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,38 @@
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Host.Context;
+using Elsa.AI.Host.Options;
+using Elsa.AI.Host.Services;
+using Elsa.AI.Host.Streaming;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddAIHostServices(this IServiceCollection services, Action? configureOptions = null)
+ {
+ if (configureOptions != null)
+ services.Configure(configureOptions);
+
+ services.AddLogging();
+ services.AddOptions();
+ services.TryAddSingleton();
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ services.TryAddSingleton();
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddScoped();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+
+ return services;
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Features/AIFeature.cs b/src/modules/Elsa.AI.Host/Features/AIFeature.cs
new file mode 100644
index 000000000..785198022
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Features/AIFeature.cs
@@ -0,0 +1,22 @@
+using Elsa.AI.Host.Options;
+using Elsa.Extensions;
+using Elsa.Features.Abstractions;
+using Elsa.Features.Services;
+
+namespace Elsa.AI.Host.Features;
+
+public class AIFeature(IModule module) : FeatureBase(module)
+{
+ public Action? ConfigureOptions { get; set; }
+
+ public override void Configure()
+ {
+ Module.AddFastEndpointsAssembly();
+ }
+
+ public override void Apply()
+ {
+ Services.AddAIHostServices(ConfigureOptions);
+ Module.AddFastEndpointsFromModule();
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/FodyWeavers.xml b/src/modules/Elsa.AI.Host/FodyWeavers.xml
new file mode 100644
index 000000000..dfa2f2e5f
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/modules/Elsa.AI.Host/Options/AIHostOptions.cs b/src/modules/Elsa.AI.Host/Options/AIHostOptions.cs
new file mode 100644
index 000000000..1c93ebdb7
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Options/AIHostOptions.cs
@@ -0,0 +1,35 @@
+namespace Elsa.AI.Host.Options;
+
+public class AIHostOptions
+{
+ public bool StreamingEnabled { get; set; } = true;
+ public bool ConversationPersistenceEnabled { get; set; } = true;
+ public bool ProposalReviewEnabled { get; set; } = true;
+ public TimeSpan ConversationRetention { get; set; } = TimeSpan.FromDays(30);
+ public TimeSpan ReconnectGrace { get; set; } = TimeSpan.FromMinutes(5);
+ public int MaxToolResultBytes { get; set; } = 64 * 1024;
+ public int MaxResolvedContextBytes { get; set; } = 128 * 1024;
+ public string? DefaultProviderName { get; set; }
+ public ICollection Providers { get; set; } = [];
+ public ICollection SupportedAttachmentKinds { get; set; } = ["WorkflowDefinition", "WorkflowInstance"];
+ public ICollection Agents { get; set; } = [new() { Name = "workflow-author", DisplayName = "Workflow author", Description = "Creates safe workflow proposals" }];
+}
+
+public class AIProviderOptions
+{
+ public string Name { get; set; } = "";
+ public string Provider { get; set; } = "";
+ public string? Model { get; set; }
+ public string? ApiKeySecretName { get; set; }
+ public string? Endpoint { get; set; }
+ public bool Enabled { get; set; } = true;
+}
+
+public class AIAgentOptions
+{
+ public string Name { get; set; } = "";
+ public string DisplayName { get; set; } = "";
+ public string Description { get; set; } = "";
+ public string? ProviderName { get; set; }
+ public ICollection Permissions { get; set; } = [];
+}
diff --git a/src/modules/Elsa.AI.Host/Options/AIProviderOptionsExtensions.cs b/src/modules/Elsa.AI.Host/Options/AIProviderOptionsExtensions.cs
new file mode 100644
index 000000000..efbd3a973
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Options/AIProviderOptionsExtensions.cs
@@ -0,0 +1,26 @@
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Host.Options;
+
+internal static class AIProviderOptionsExtensions
+{
+ public static bool IsProviderEnabled(this IReadOnlyCollection providerOptions, string providerName)
+ {
+ if (providerOptions.Count == 0)
+ return true;
+
+ return providerOptions.Any(x => x.Enabled &&
+ (string.Equals(x.Name, providerName, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(x.Provider, providerName, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ public static AIProviderConfiguration ToProviderConfiguration(this AIProviderOptions options) =>
+ new()
+ {
+ Name = options.Name,
+ Provider = options.Provider,
+ Model = options.Model,
+ ApiKeySecretName = options.ApiKeySecretName,
+ Endpoint = options.Endpoint
+ };
+}
diff --git a/src/modules/Elsa.AI.Host/Permissions/AIPermissions.cs b/src/modules/Elsa.AI.Host/Permissions/AIPermissions.cs
new file mode 100644
index 000000000..1c26543c6
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Permissions/AIPermissions.cs
@@ -0,0 +1,12 @@
+namespace Elsa.AI.Host.Permissions;
+
+public static class AIPermissions
+{
+ public const string Chat = "ai:chat";
+ public const string ViewTools = "ai:tools:view";
+ public const string ViewCapabilities = "ai:capabilities:view";
+ public const string ViewProposals = "ai:proposals:view";
+ public const string ApproveProposals = "ai:proposals:approve";
+ public const string ApplyProposals = "ai:proposals:apply";
+ public const string ManageTools = "ai:tools:manage";
+}
diff --git a/src/modules/Elsa.AI.Host/Services/AIAuditSink.cs b/src/modules/Elsa.AI.Host/Services/AIAuditSink.cs
new file mode 100644
index 000000000..9dea6a289
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Services/AIAuditSink.cs
@@ -0,0 +1,35 @@
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.AI.Host.Services;
+
+public class AIAuditSink(IServiceScopeFactory scopeFactory, ILogger logger) : IAIAuditSink
+{
+ public async ValueTask RecordAsync(AIAuditEvent auditEvent, CancellationToken cancellationToken = default)
+ {
+ await RecordManyAsync([auditEvent], cancellationToken);
+ }
+
+ public async ValueTask RecordManyAsync(IReadOnlyCollection auditEvents, CancellationToken cancellationToken = default)
+ {
+ if (auditEvents.Count == 0)
+ return;
+
+ using var scope = scopeFactory.CreateScope();
+ var handlers = scope.ServiceProvider.GetServices();
+
+ foreach (var handler in handlers)
+ {
+ try
+ {
+ await handler.RecordManyAsync(auditEvents, cancellationToken);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ logger.LogWarning(e, "AI audit handler {HandlerType} failed while recording {AuditEventCount} event(s).", handler.GetType().Name, auditEvents.Count);
+ }
+ }
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Services/AIContextProviderValidationHostedService.cs b/src/modules/Elsa.AI.Host/Services/AIContextProviderValidationHostedService.cs
new file mode 100644
index 000000000..796732eb8
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Services/AIContextProviderValidationHostedService.cs
@@ -0,0 +1,26 @@
+using Elsa.AI.Abstractions.Contracts;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Elsa.AI.Host.Services;
+
+public class AIContextProviderValidationHostedService(
+ IServiceScopeFactory scopeFactory,
+ ILogger logger) : IHostedService
+{
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ using var scope = scopeFactory.CreateScope();
+ var providers = scope.ServiceProvider.GetServices();
+
+ foreach (var group in providers.GroupBy(x => x.Kind, StringComparer.OrdinalIgnoreCase).Where(x => x.Count() > 1))
+ logger.LogWarning(
+ "Multiple AI context providers are registered for kind {ContextKind}. The last non-placeholder provider will be used when available.",
+ group.Key);
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
diff --git a/src/modules/Elsa.AI.Host/Services/AIOrchestrator.cs b/src/modules/Elsa.AI.Host/Services/AIOrchestrator.cs
new file mode 100644
index 000000000..e9873ad95
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Services/AIOrchestrator.cs
@@ -0,0 +1,761 @@
+using System.Text;
+using System.Text.Json;
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+using Elsa.AI.Host.Context;
+using Elsa.AI.Host.Options;
+using Elsa.AI.Host.Streaming;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Elsa.AI.Host.Services;
+
+public class AIOrchestrator(
+ IEnumerable providers,
+ IAIToolRegistry toolRegistry,
+ IAIConversationStore conversationStore,
+ AIContextResolver contextResolver,
+ AIStreamEventMapper streamEventMapper,
+ IAIAuditSink auditSink,
+ ILogger logger,
+ IOptions options) : IAIOrchestrator
+{
+ private const int MaxProviderTurns = 8;
+
+ public async IAsyncEnumerable ExecuteChatAsync(AIChatRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var conversationId = request.ConversationId ?? Guid.NewGuid().ToString("N");
+ var sequence = 0L;
+ var providerSelection = SelectProvider(request);
+ var provider = providerSelection.Provider;
+ var conversationPersistenceEnabled = options.Value.ConversationPersistenceEnabled;
+ AIConversation? conversation = null;
+ Exception? preparationError = null;
+ if (conversationPersistenceEnabled)
+ {
+ try
+ {
+ conversation = await conversationStore.FindAsync(conversationId, cancellationToken);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ preparationError = e;
+ }
+ }
+
+ if (conversation != null && (!BelongsToTenant(conversation, request.TenantId) || !BelongsToUser(conversation, request.UserId)))
+ {
+ conversation = null;
+ conversationId = Guid.NewGuid().ToString("N");
+ }
+ var messages = conversation?.Messages.ToList() ?? [];
+ if (request.IsReconnect && messages.Count == 0)
+ conversationId = Guid.NewGuid().ToString("N");
+
+ if (request.IsReconnect && IsCompletedReconnect(conversation, request.Message))
+ {
+ var nextSequence = GetNextSequence(messages);
+ if (conversation!.Status == AIConversationStatus.Failed)
+ {
+ var lastAssistantContent = conversation.Messages.LastOrDefault(x => x.Role == AIMessageRole.Assistant)?.Content;
+ if (!string.IsNullOrEmpty(lastAssistantContent))
+ yield return CreateEvent("conversation.error", conversationId, nextSequence++, new JsonObject
+ {
+ ["content"] = lastAssistantContent
+ });
+ }
+
+ yield return CreateEvent("conversation.completed", conversationId, nextSequence);
+ yield break;
+ }
+
+ var providerSessionId = conversation?.ProviderSessionId;
+
+ if (preparationError == null && provider != null && string.IsNullOrWhiteSpace(providerSessionId))
+ {
+ try
+ {
+ var session = await provider.CreateSessionAsync(new CreateAISessionRequest
+ {
+ ConversationId = conversationId,
+ Agent = request.Agent,
+ TenantId = request.TenantId,
+ ProviderConfiguration = providerSelection.Configuration
+ }, cancellationToken);
+ providerSessionId = session.ProviderSessionId ?? session.Id;
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ preparationError = e;
+ }
+ }
+
+ var isDuplicateReconnectMessage = request.IsReconnect && HasReconnectUserMessage(conversation, request.Message);
+ var providerHistory = messages.ToList();
+ if (request.IsReconnect && messages.Count > 0)
+ sequence = GetNextSequence(messages);
+
+ yield return CreateEvent("conversation.started", conversationId, sequence++);
+
+ var userMessage = isDuplicateReconnectMessage
+ ? messages.Last(x => x.Role == AIMessageRole.User && string.Equals(NormalizeMessage(x.Content), NormalizeMessage(request.Message), StringComparison.Ordinal))
+ : CreateMessage(conversationId, AIMessageRole.User, request.Message, sequence++);
+
+ if (!isDuplicateReconnectMessage)
+ messages.Add(userMessage);
+
+ var knownToolCallIds = RestoreToolResults(messages).Select(x => x.ToolCallId).ToHashSet(StringComparer.OrdinalIgnoreCase);
+ var pendingToolResults = isDuplicateReconnectMessage ? RestorePendingToolResults(messages) : new List();
+
+ await TrySaveConversationAsync(conversationId, request, AIConversationStatus.Active, messages, conversation, providerSessionId, cancellationToken);
+ await RecordChatAuditAsync("chat.started", request, conversationId, provider?.Name, cancellationToken);
+
+ IReadOnlyCollection context = [];
+ IReadOnlyCollection tools = [];
+
+ if (preparationError == null)
+ {
+ try
+ {
+ context = LimitResolvedContext(await contextResolver.ResolveAsync(request, cancellationToken));
+ tools = await toolRegistry.ListAsync(new AIToolQuery
+ {
+ Agent = request.Agent,
+ ActorId = request.UserId,
+ TenantId = request.TenantId,
+ UserPermissions = request.UserPermissions
+ }, cancellationToken);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ preparationError = e;
+ }
+ }
+
+ if (preparationError != null)
+ {
+ const string content = "Weaver could not prepare AI context or tools for this request.";
+ logger.LogWarning(preparationError, "Failed to prepare AI chat context or tools for conversation {ConversationId}.", conversationId);
+ yield return CreateEvent("conversation.error", conversationId, sequence++, new JsonObject
+ {
+ ["content"] = content
+ });
+ messages.Add(CreateMessage(conversationId, AIMessageRole.Assistant, content, sequence - 1));
+ await TrySaveConversationAsync(conversationId, request, AIConversationStatus.Failed, messages, conversation, providerSessionId, cancellationToken);
+ await RecordChatAuditAsync("chat.failed", request, conversationId, provider?.Name, cancellationToken);
+ yield return CreateEvent("conversation.completed", conversationId, sequence);
+ yield break;
+ }
+
+ if (provider == null)
+ {
+ const string content = "Weaver is ready, but no AI provider is configured.";
+ yield return CreateEvent("assistant.delta", conversationId, sequence++, new JsonObject
+ {
+ ["content"] = content
+ });
+ messages.Add(CreateMessage(conversationId, AIMessageRole.Assistant, content, sequence - 1));
+ }
+ else
+ {
+ var assistantContent = new StringBuilder();
+
+ for (var turn = 0; turn < MaxProviderTurns; turn++)
+ {
+ var currentTurnToolResults = new List();
+ var currentTurnMessages = new List();
+ var currentTurnToolMessages = new List();
+ assistantContent.Clear();
+
+ var turnRequest = new AITurnRequest
+ {
+ ConversationId = conversationId,
+ ProviderSessionId = providerSessionId,
+ Message = turn == 0 && !isDuplicateReconnectMessage ? request.Message : "",
+ Messages = providerHistory.ToList(),
+ Context = context,
+ Tools = tools.Where(x => x.IsEnabled).ToList(),
+ ToolResults = GetUnrepresentedToolResults(pendingToolResults, providerHistory),
+ Agent = request.Agent,
+ ProviderConfiguration = providerSelection.Configuration
+ };
+ Exception? providerTurnError = null;
+ await foreach (var providerRead in ReadProviderEventsAsync(provider.ExecuteTurnAsync(turnRequest, cancellationToken), cancellationToken))
+ {
+ if (providerRead.Error != null)
+ {
+ providerTurnError = providerRead.Error;
+ break;
+ }
+
+ var providerEvent = providerRead.Event!;
+ var streamEvent = streamEventMapper.Map(conversationId, providerEvent) with { Sequence = sequence++ };
+ yield return streamEvent;
+
+ if (TryReadAssistantContent(providerEvent, out var content))
+ assistantContent.Append(content);
+
+ if (!TryReadToolCall(providerEvent, out var toolCall))
+ continue;
+
+ if (knownToolCallIds.Contains(toolCall.Id) ||
+ currentTurnToolResults.Any(x => string.Equals(x.ToolCallId, toolCall.Id, StringComparison.OrdinalIgnoreCase)))
+ continue;
+
+ var toolExecution = await ExecuteToolCallAsync(toolCall, request, conversationId, sequence++, cancellationToken);
+ yield return toolExecution.StreamEvent;
+
+ currentTurnToolResults.Add(toolExecution.TurnResult);
+ var toolMessage = CreateMessage(conversationId, AIMessageRole.Tool, toolExecution.TurnResult.Result.Summary, toolExecution.StreamEvent.Sequence, new JsonObject
+ {
+ ["toolCallId"] = toolExecution.TurnResult.ToolCallId,
+ ["toolName"] = toolExecution.TurnResult.ToolName,
+ ["status"] = toolExecution.TurnResult.Result.Status.ToString()
+ });
+ currentTurnToolMessages.Add(toolMessage);
+ }
+
+ if (providerTurnError != null)
+ {
+ const string content = "Weaver could not complete the AI provider turn for this request.";
+ logger.LogWarning(providerTurnError, "Failed to execute AI provider turn for conversation {ConversationId}.", conversationId);
+ yield return CreateEvent("conversation.error", conversationId, sequence++, new JsonObject
+ {
+ ["content"] = content
+ });
+ messages.Add(CreateMessage(conversationId, AIMessageRole.Assistant, content, sequence - 1));
+ await TrySaveConversationAsync(conversationId, request, AIConversationStatus.Failed, messages, conversation, providerSessionId, cancellationToken);
+ await RecordChatAuditAsync("chat.failed", request, conversationId, provider.Name, cancellationToken);
+ yield return CreateEvent("conversation.completed", conversationId, sequence);
+ yield break;
+ }
+
+ if (assistantContent.Length > 0 || currentTurnToolMessages.Count > 0)
+ {
+ var assistantSequence = currentTurnToolMessages.Count > 0
+ ? currentTurnToolMessages.Min(x => x.StreamSequence) - 1
+ : sequence - 1;
+ var assistantMessage = CreateMessage(conversationId, AIMessageRole.Assistant, assistantContent.ToString(), assistantSequence, CreateAssistantToolCallMetadata(currentTurnToolResults));
+ messages.Add(assistantMessage);
+ currentTurnMessages.Add(assistantMessage);
+ }
+
+ messages.AddRange(currentTurnToolMessages);
+ currentTurnMessages.AddRange(currentTurnToolMessages);
+
+ if (currentTurnToolResults.Count == 0)
+ break;
+
+ foreach (var toolResult in currentTurnToolResults)
+ knownToolCallIds.Add(toolResult.ToolCallId);
+
+ pendingToolResults = currentTurnToolResults;
+ if (providerHistory.All(x => x.Id != userMessage.Id))
+ providerHistory.Add(userMessage);
+
+ providerHistory.AddRange(currentTurnMessages);
+ await TrySaveConversationAsync(conversationId, request, AIConversationStatus.Active, messages, conversation, providerSessionId, cancellationToken);
+
+ if (turn == MaxProviderTurns - 1)
+ {
+ const string content = "Tool execution stopped because the provider requested too many continuation turns.";
+ yield return CreateEvent("assistant.delta", conversationId, sequence++, new JsonObject
+ {
+ ["content"] = content
+ });
+ messages.Add(CreateMessage(conversationId, AIMessageRole.Assistant, content, sequence - 1));
+ break;
+ }
+ }
+ }
+
+ await TrySaveConversationAsync(conversationId, request, AIConversationStatus.Completed, messages, conversation, providerSessionId, cancellationToken);
+ await RecordChatAuditAsync("chat.completed", request, conversationId, provider?.Name, cancellationToken);
+ yield return CreateEvent("conversation.completed", conversationId, sequence);
+ }
+
+ private static AIStreamEvent CreateEvent(string type, string conversationId, long sequence, JsonObject? data = null) =>
+ new()
+ {
+ Type = type,
+ ConversationId = conversationId,
+ Sequence = sequence,
+ Timestamp = DateTimeOffset.UtcNow,
+ Data = data ?? []
+ };
+
+ private static async IAsyncEnumerable ReadProviderEventsAsync(
+ IAsyncEnumerable providerEvents,
+ [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var enumerator = providerEvents.GetAsyncEnumerator(cancellationToken);
+ try
+ {
+ while (true)
+ {
+ AIProviderEvent? providerEvent = null;
+ Exception? error = null;
+ var hasEvent = false;
+
+ try
+ {
+ hasEvent = await enumerator.MoveNextAsync();
+ if (hasEvent)
+ providerEvent = enumerator.Current;
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ error = e;
+ }
+
+ if (error != null)
+ {
+ yield return new ProviderReadResult(null, error);
+ yield break;
+ }
+
+ if (!hasEvent)
+ yield break;
+
+ yield return new ProviderReadResult(providerEvent, null);
+ }
+ }
+ finally
+ {
+ await enumerator.DisposeAsync();
+ }
+ }
+
+ private ProviderSelection SelectProvider(AIChatRequest request)
+ {
+ var providerOptions = options.Value.Providers.ToList();
+ var configuredProviders = providerOptions.Where(x => x.Enabled).ToList();
+ var availableProviders = providers
+ .Where(x => providerOptions.IsProviderEnabled(x.Name))
+ .ToList();
+ var providerName = request.ProviderName ?? FindAgentProviderName(request.Agent) ?? options.Value.DefaultProviderName;
+
+ if (!string.IsNullOrWhiteSpace(providerName))
+ {
+ var configuredProvider = configuredProviders.FirstOrDefault(x => string.Equals(x.Name, providerName, StringComparison.OrdinalIgnoreCase));
+ var provider = configuredProvider != null
+ ? availableProviders.FirstOrDefault(x => string.Equals(x.Name, configuredProvider.Name, StringComparison.OrdinalIgnoreCase)) ??
+ availableProviders.FirstOrDefault(x => string.Equals(x.Name, configuredProvider.Provider, StringComparison.OrdinalIgnoreCase))
+ : availableProviders.FirstOrDefault(x => string.Equals(x.Name, providerName, StringComparison.OrdinalIgnoreCase));
+
+ return new ProviderSelection(provider, configuredProvider?.ToProviderConfiguration());
+ }
+
+ if (availableProviders.Count != 1)
+ {
+ if (availableProviders.Count > 1)
+ logger.LogWarning(
+ "Multiple AI providers are available ({ProviderNames}) but no default provider name is configured. Set AIHostOptions.DefaultProviderName to select one.",
+ string.Join(", ", availableProviders.Select(x => x.Name)));
+
+ return new ProviderSelection(null, null);
+ }
+
+ var selectedProvider = availableProviders[0];
+ var selectedConfiguration = configuredProviders.FirstOrDefault(x => string.Equals(x.Name, selectedProvider.Name, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(x.Provider, selectedProvider.Name, StringComparison.OrdinalIgnoreCase));
+
+ return new ProviderSelection(selectedProvider, selectedConfiguration?.ToProviderConfiguration());
+ }
+
+ private string? FindAgentProviderName(string? agent) =>
+ string.IsNullOrWhiteSpace(agent)
+ ? null
+ : options.Value.Agents.FirstOrDefault(x => string.Equals(x.Name, agent, StringComparison.OrdinalIgnoreCase))?.ProviderName;
+
+ private async ValueTask RecordChatAuditAsync(string type, AIChatRequest request, string conversationId, string? providerName, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await auditSink.RecordAsync(new AIAuditEvent
+ {
+ Type = type,
+ TenantId = request.TenantId,
+ ActorId = request.UserId,
+ ConversationId = conversationId,
+ Timestamp = DateTimeOffset.UtcNow,
+ Summary = type switch
+ {
+ "chat.started" => "Chat started",
+ "chat.failed" => "Chat failed",
+ _ => "Chat completed"
+ },
+ Data = new JsonObject
+ {
+ ["agent"] = request.Agent,
+ ["provider"] = providerName,
+ ["attachmentCount"] = request.Attachments.Count
+ }
+ }, cancellationToken);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ logger.LogWarning(e, "Failed to record AI chat audit event {AuditEventType} for conversation {ConversationId}.", type, conversationId);
+ }
+ }
+
+ private async ValueTask ExecuteToolCallAsync(ToolCall toolCall, AIChatRequest request, string conversationId, long sequence, CancellationToken cancellationToken)
+ {
+ var tool = await toolRegistry.FindAsync(toolCall.Name, new AIToolQuery
+ {
+ Agent = request.Agent,
+ ActorId = request.UserId,
+ TenantId = request.TenantId,
+ UserPermissions = request.UserPermissions
+ }, cancellationToken);
+ if (tool == null)
+ {
+ var result = new AIToolResult { Status = AIToolInvocationStatus.Failed, Error = $"Tool '{toolCall.Name}' was not found." };
+ await RecordToolAuditEventsAsync(request, conversationId, toolCall, ["tool.failed"], cancellationToken);
+ return CreateToolExecutionResult(conversationId, sequence, toolCall, result);
+ }
+
+ using var toolScope = tool;
+ try
+ {
+ await RecordToolAuditEventsAsync(request, conversationId, toolCall, ["tool.invoked"], cancellationToken);
+ var result = await tool.ExecuteAsync(new AIToolExecutionContext
+ {
+ ConversationId = conversationId,
+ TenantId = request.TenantId,
+ ActorId = request.UserId,
+ Agent = request.Agent,
+ Arguments = toolCall.Arguments
+ }, cancellationToken);
+ await RecordToolAuditEventsAsync(request, conversationId, toolCall, ["tool.completed"], cancellationToken);
+
+ return CreateToolExecutionResult(conversationId, sequence, toolCall, LimitToolResult(result));
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ logger.LogWarning(e, "AI tool {ToolName} failed for conversation {ConversationId}.", toolCall.Name, conversationId);
+ await RecordToolAuditEventsAsync(request, conversationId, toolCall, ["tool.failed"], cancellationToken);
+ return CreateToolExecutionResult(conversationId, sequence, toolCall, new AIToolResult { Status = AIToolInvocationStatus.Failed, Error = "Tool execution failed." });
+ }
+ }
+
+ private async ValueTask RecordToolAuditEventsAsync(AIChatRequest request, string conversationId, ToolCall toolCall, IReadOnlyCollection types, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await auditSink.RecordManyAsync(types.Select(type => CreateToolAuditEvent(type, request, conversationId, toolCall)).ToList(), cancellationToken);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ logger.LogWarning(e, "Failed to record AI tool audit events for tool {ToolName}.", toolCall.Name);
+ }
+ }
+
+ private static AIAuditEvent CreateToolAuditEvent(string type, AIChatRequest request, string conversationId, ToolCall toolCall) =>
+ new()
+ {
+ Type = type,
+ TenantId = request.TenantId,
+ ActorId = request.UserId,
+ ConversationId = conversationId,
+ ToolInvocationId = toolCall.Id,
+ Timestamp = DateTimeOffset.UtcNow,
+ Summary = $"{toolCall.Name} {type}",
+ Data = new JsonObject
+ {
+ ["toolName"] = toolCall.Name
+ }
+ };
+
+ private static AIStreamEvent CreateToolResultEvent(string conversationId, long sequence, ToolCall toolCall, AIToolResult result) =>
+ CreateEvent("tool.result", conversationId, sequence, new JsonObject
+ {
+ ["toolCallId"] = toolCall.Id,
+ ["toolName"] = toolCall.Name,
+ ["status"] = result.Status.ToString(),
+ ["summary"] = result.Summary,
+ ["error"] = result.Error,
+ ["data"] = result.Data.DeepClone()
+ });
+
+ private static ToolExecutionResult CreateToolExecutionResult(string conversationId, long sequence, ToolCall toolCall, AIToolResult result) =>
+ new(CreateToolResultEvent(conversationId, sequence, toolCall, result), new AIToolTurnResult
+ {
+ ToolCallId = toolCall.Id,
+ ToolName = toolCall.Name,
+ Result = result
+ });
+
+ private IReadOnlyCollection LimitResolvedContext(IReadOnlyCollection contexts)
+ {
+ var maxBytes = options.Value.MaxResolvedContextBytes;
+ if (maxBytes <= 0)
+ return contexts;
+
+ var limited = new List();
+ var usedBytes = 0;
+
+ foreach (var context in contexts)
+ {
+ var contextSize = GetUtf8Size(context);
+ if (usedBytes + contextSize <= maxBytes)
+ {
+ usedBytes += contextSize;
+ limited.Add(context);
+ continue;
+ }
+
+ if (limited.Count > 0)
+ {
+ logger.LogDebug(
+ "Dropping AI resolved context {ContextKind}/{ReferenceId} because resolved context exceeds the configured {MaxResolvedContextBytes} byte budget.",
+ context.Kind,
+ context.ReferenceId,
+ maxBytes);
+ continue;
+ }
+
+ limited.Add(TruncateContext(context, maxBytes));
+ break;
+ }
+
+ return limited;
+ }
+
+ private static AIResolvedContext TruncateContext(AIResolvedContext context, int maxBytes) =>
+ context with
+ {
+ Summary = Truncate(context.Summary, maxBytes),
+ Data = CreateTruncatedPayload(maxBytes),
+ Metadata = CreateTruncatedPayload(maxBytes)
+ };
+
+ private AIToolResult LimitToolResult(AIToolResult result)
+ {
+ if (GetUtf8Size(result) <= options.Value.MaxToolResultBytes)
+ return result;
+
+ return result with
+ {
+ Summary = Truncate(result.Summary, options.Value.MaxToolResultBytes),
+ Data = CreateTruncatedPayload(options.Value.MaxToolResultBytes)
+ };
+ }
+
+ private static JsonObject CreateTruncatedPayload(int maxBytes) =>
+ new()
+ {
+ ["truncated"] = true,
+ ["maxBytes"] = maxBytes
+ };
+
+ private static int GetUtf8Size(T value) =>
+ JsonSerializer.SerializeToUtf8Bytes(value).Length;
+
+ private static string Truncate(string value, int maxBytes)
+ {
+ if (string.IsNullOrEmpty(value))
+ return value;
+
+ if (maxBytes <= 0)
+ return "";
+
+ if (Encoding.UTF8.GetByteCount(value) <= maxBytes)
+ return value;
+
+ var low = 0;
+ var high = Math.Min(value.Length, maxBytes);
+ while (low < high)
+ {
+ var candidate = (low + high + 1) / 2;
+ if (Encoding.UTF8.GetByteCount(value.AsSpan(0, candidate)) <= maxBytes)
+ low = candidate;
+ else
+ high = candidate - 1;
+ }
+
+ if (low > 0 && char.IsHighSurrogate(value[low - 1]))
+ low--;
+
+ return value[..low];
+ }
+
+ private static bool TryReadToolCall(AIProviderEvent providerEvent, out ToolCall toolCall)
+ {
+ toolCall = default;
+ if (!string.Equals(providerEvent.Type, "tool.call", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ var name = providerEvent.Data["toolName"]?.GetValue() ?? providerEvent.Data["name"]?.GetValue();
+ if (string.IsNullOrWhiteSpace(name))
+ return false;
+
+ var id = providerEvent.Data["id"]?.GetValue() ?? Guid.NewGuid().ToString("N");
+ var arguments = providerEvent.Data["arguments"]?.DeepClone() as JsonObject ?? [];
+ toolCall = new ToolCall(id, name, arguments);
+ return true;
+ }
+
+ private static bool TryReadAssistantContent(AIProviderEvent providerEvent, out string content)
+ {
+ content = "";
+ if (!string.Equals(providerEvent.Type, "assistant.delta", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ content = providerEvent.Data["content"]?.GetValue() ?? "";
+ return !string.IsNullOrEmpty(content);
+ }
+
+ private static AIMessage CreateMessage(string conversationId, AIMessageRole role, string content, long streamSequence, JsonObject? metadata = null) =>
+ new()
+ {
+ Id = Guid.NewGuid().ToString("N"),
+ ConversationId = conversationId,
+ Role = role,
+ Content = content,
+ CreatedAt = DateTimeOffset.UtcNow,
+ StreamSequence = streamSequence,
+ Metadata = metadata ?? []
+ };
+
+ private static JsonObject? CreateAssistantToolCallMetadata(IReadOnlyCollection toolResults)
+ {
+ if (toolResults.Count == 0)
+ return null;
+
+ var toolCallIds = new JsonArray();
+ foreach (var toolResult in toolResults)
+ toolCallIds.Add(toolResult.ToolCallId);
+
+ return new JsonObject
+ {
+ ["toolCallIds"] = toolCallIds
+ };
+ }
+
+ private static bool HasReconnectUserMessage(AIConversation? conversation, string message)
+ {
+ return conversation is { Status: AIConversationStatus.Active } &&
+ HasUserMessage(conversation, message);
+ }
+
+ private static bool IsCompletedReconnect(AIConversation? conversation, string message) =>
+ conversation is { Status: AIConversationStatus.Completed or AIConversationStatus.Failed } && HasUserMessage(conversation, message);
+
+ private static bool HasUserMessage(AIConversation conversation, string message) =>
+ conversation.Messages.Any(x => x.Role == AIMessageRole.User && string.Equals(NormalizeMessage(x.Content), NormalizeMessage(message), StringComparison.Ordinal));
+
+ private static long GetNextSequence(IReadOnlyCollection messages) =>
+ messages.Count == 0 ? 0 : messages.Max(x => x.StreamSequence) + 1;
+
+ private static List RestoreToolResults(IEnumerable messages)
+ {
+ return messages
+ .Where(x => x.Role == AIMessageRole.Tool)
+ .Select(CreateToolTurnResult)
+ .OfType()
+ .ToList();
+ }
+
+ private static List RestorePendingToolResults(IReadOnlyCollection messages)
+ {
+ return messages
+ .Reverse()
+ .TakeWhile(x => x.Role == AIMessageRole.Tool)
+ .Reverse()
+ .Select(CreateToolTurnResult)
+ .OfType()
+ .ToList();
+ }
+
+ private static IReadOnlyCollection GetUnrepresentedToolResults(IReadOnlyCollection toolResults, IReadOnlyCollection messages)
+ {
+ if (toolResults.Count == 0)
+ return [];
+
+ var representedToolCallIds = messages
+ .Where(x => x.Role == AIMessageRole.Tool)
+ .Select(x => x.Metadata["toolCallId"]?.GetValue())
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ return toolResults
+ .Where(x => !representedToolCallIds.Contains(x.ToolCallId))
+ .ToList();
+ }
+
+ private static AIToolTurnResult? CreateToolTurnResult(AIMessage message)
+ {
+ var toolCallId = message.Metadata["toolCallId"]?.GetValue();
+ var toolName = message.Metadata["toolName"]?.GetValue();
+
+ if (string.IsNullOrWhiteSpace(toolCallId) || string.IsNullOrWhiteSpace(toolName))
+ return null;
+
+ var status = Enum.TryParse(message.Metadata["status"]?.GetValue(), out var parsedStatus)
+ ? parsedStatus
+ : AIToolInvocationStatus.Completed;
+
+ return new AIToolTurnResult
+ {
+ ToolCallId = toolCallId,
+ ToolName = toolName,
+ Result = new AIToolResult
+ {
+ Status = status,
+ Summary = message.Content
+ }
+ };
+ }
+
+ private static string NormalizeMessage(string message) =>
+ message.ReplaceLineEndings("\n").Trim();
+
+ private static bool BelongsToTenant(AIConversation conversation, string? tenantId) =>
+ string.Equals(NormalizeTenantId(conversation.TenantId), NormalizeTenantId(tenantId), StringComparison.Ordinal);
+
+ private static bool BelongsToUser(AIConversation conversation, string userId) =>
+ string.IsNullOrWhiteSpace(conversation.UserId) || string.Equals(conversation.UserId, userId, StringComparison.Ordinal);
+
+ private static string NormalizeTenantId(string? tenantId) =>
+ string.IsNullOrWhiteSpace(tenantId) ? "" : tenantId;
+
+ private async ValueTask TrySaveConversationAsync(string conversationId, AIChatRequest request, AIConversationStatus status, IReadOnlyCollection messages, AIConversation? conversation, string? providerSessionId, CancellationToken cancellationToken)
+ {
+ if (!options.Value.ConversationPersistenceEnabled)
+ return;
+
+ try
+ {
+ var now = DateTimeOffset.UtcNow;
+ var retentionMode = conversation?.RetentionMode ?? AIRetentionMode.Configured;
+ DateTimeOffset? retentionExpiresAt = retentionMode == AIRetentionMode.Configured
+ ? now.Add(options.Value.ConversationRetention)
+ : null;
+
+ await conversationStore.SaveAsync(new AIConversation
+ {
+ Id = conversationId,
+ TenantId = request.TenantId,
+ UserId = request.UserId,
+ Title = conversation?.Title,
+ Status = status,
+ CreatedAt = conversation is null || conversation.CreatedAt == default ? now : conversation.CreatedAt,
+ UpdatedAt = now,
+ ProviderSessionId = providerSessionId ?? conversation?.ProviderSessionId,
+ RetentionMode = retentionMode,
+ RetentionExpiresAt = retentionExpiresAt,
+ Messages = messages.ToList()
+ }, cancellationToken);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ logger.LogWarning(e, "Failed to persist AI conversation {ConversationId} with status {ConversationStatus}.", conversationId, status);
+ }
+ }
+
+ private readonly record struct ToolCall(string Id, string Name, JsonObject Arguments);
+ private readonly record struct ToolExecutionResult(AIStreamEvent StreamEvent, AIToolTurnResult TurnResult);
+ private readonly record struct ProviderReadResult(AIProviderEvent? Event, Exception? Error);
+ private readonly record struct ProviderSelection(IAIProvider? Provider, AIProviderConfiguration? Configuration);
+}
diff --git a/src/modules/Elsa.AI.Host/Services/AIToolEnablementService.cs b/src/modules/Elsa.AI.Host/Services/AIToolEnablementService.cs
new file mode 100644
index 000000000..f2d3c1b5d
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Services/AIToolEnablementService.cs
@@ -0,0 +1,49 @@
+using System.Collections.Concurrent;
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Host.Services;
+
+public class AIToolEnablementService
+{
+ private readonly ConcurrentDictionary _enabledToolNames = new(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary _enabledAdministrativeToolNames = new(StringComparer.OrdinalIgnoreCase);
+
+ public bool IsEnabled(AIToolDefinition definition)
+ {
+ if (definition.Mutability == AIToolMutability.Administrative)
+ return _enabledAdministrativeToolNames.ContainsKey(definition.Name);
+
+ if (definition.Mutability == AIToolMutability.Proposal)
+ return _enabledToolNames.ContainsKey(definition.Name);
+
+ if (definition.Mutability == AIToolMutability.ReadOnly)
+ return true;
+
+ if (definition.EnabledByDefault)
+ return true;
+
+ return _enabledToolNames.ContainsKey(definition.Name);
+ }
+
+ ///
+ /// Enables a non-administrative tool by name. Administrative tools remain disabled by this service and must be exposed through a dedicated governed path.
+ ///
+ public void Enable(string toolName)
+ {
+ _enabledToolNames.TryAdd(toolName, 0);
+ }
+
+ ///
+ /// Enables an administrative tool by name. Callers must wrap this in their own governed approval and audit path.
+ ///
+ public void EnableAdministrative(string toolName)
+ {
+ _enabledAdministrativeToolNames.TryAdd(toolName, 0);
+ }
+
+ public void Disable(string toolName)
+ {
+ _enabledToolNames.TryRemove(toolName, out _);
+ _enabledAdministrativeToolNames.TryRemove(toolName, out _);
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Services/AIToolRegistry.cs b/src/modules/Elsa.AI.Host/Services/AIToolRegistry.cs
new file mode 100644
index 000000000..ed0966ae3
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Services/AIToolRegistry.cs
@@ -0,0 +1,267 @@
+using System.Collections.Concurrent;
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.AI.Host.Services;
+
+public class AIToolRegistry(IServiceScopeFactory scopeFactory, AIToolEnablementService enablementService) : IAIToolRegistry
+{
+ private readonly object _definitionCacheLock = new();
+ private readonly ConcurrentDictionary _toolTypes = new(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary _uncacheableToolNames = new(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary _uncacheableToolTypes = new();
+ private volatile IReadOnlyCollection? _definitions;
+
+ public ValueTask> ListAsync(AIToolQuery query, CancellationToken cancellationToken = default)
+ {
+ var definitions = GetCachedDefinitions()
+ .Where(x => IsVisible(x, query))
+ .Select(x => x with { IsEnabled = enablementService.IsEnabled(x) })
+ .ToList();
+
+ return ValueTask.FromResult>(definitions);
+ }
+
+ public ValueTask FindAsync(string name, AIToolQuery query, CancellationToken cancellationToken = default)
+ {
+ var scope = scopeFactory.CreateScope();
+ ResolvedTool? resolvedTool = null;
+ try
+ {
+ resolvedTool = ResolveCachedTool(scope.ServiceProvider, name) ?? ResolveAndCacheTool(scope.ServiceProvider, name);
+ if (resolvedTool == null || !TryGetDefinition(resolvedTool.Tool, out var definition) || !IsVisible(definition, query) || !enablementService.IsEnabled(definition))
+ {
+ resolvedTool?.Dispose();
+ scope.Dispose();
+ return ValueTask.FromResult(null);
+ }
+
+ return ValueTask.FromResult(new ScopedAITool(scope, resolvedTool.Tool, resolvedTool.DisposeTool));
+ }
+ catch
+ {
+ resolvedTool?.Dispose();
+ scope.Dispose();
+ throw;
+ }
+ }
+
+ private ResolvedTool? ResolveCachedTool(IServiceProvider serviceProvider, string name)
+ {
+ if (!_toolTypes.TryGetValue(name, out var toolType))
+ return null;
+
+ ResolvedTool resolvedTool;
+ try
+ {
+ if (serviceProvider.GetService(toolType) is IAITool scopeOwnedTool)
+ resolvedTool = new ResolvedTool(scopeOwnedTool, DisposeTool: false);
+ else if (ActivatorUtilities.CreateInstance(serviceProvider, toolType) is IAITool createdTool)
+ resolvedTool = new ResolvedTool(createdTool, DisposeTool: true);
+ else
+ return null;
+ }
+ catch (InvalidOperationException)
+ {
+ _uncacheableToolNames[name] = true;
+ _toolTypes.TryRemove(name, out _);
+ return null;
+ }
+
+ if (!TryGetDefinition(resolvedTool.Tool, out var definition))
+ {
+ resolvedTool.Dispose();
+ _uncacheableToolNames[name] = true;
+ _toolTypes.TryRemove(name, out _);
+ return null;
+ }
+
+ if (string.Equals(definition.Name, name, StringComparison.OrdinalIgnoreCase))
+ return resolvedTool;
+
+ resolvedTool.Dispose();
+ return null;
+ }
+
+ private ResolvedTool? ResolveAndCacheTool(IServiceProvider serviceProvider, string name)
+ {
+ var tools = serviceProvider.GetServices().ToList();
+ var toolInfos = GetToolInfos(tools);
+ UpdateToolTypeCache(toolInfos);
+ var tool = toolInfos.FirstOrDefault(x => string.Equals(x.Definition.Name, name, StringComparison.OrdinalIgnoreCase))?.Tool;
+ return tool == null ? null : new ResolvedTool(tool, DisposeTool: false);
+ }
+
+ private IReadOnlyCollection GetCachedDefinitions()
+ {
+ if (_definitions != null)
+ return _definitions;
+
+ lock (_definitionCacheLock)
+ {
+ if (_definitions != null)
+ return _definitions;
+
+ using var scope = scopeFactory.CreateScope();
+ var tools = scope.ServiceProvider.GetServices().ToList();
+ var toolInfos = GetToolInfos(tools);
+ UpdateToolTypeCache(toolInfos);
+ // Definitions are cached by design. Runtime enablement is refreshed in ListAsync; dynamic Definition properties are not.
+ _definitions = toolInfos.Select(x => x.Definition).ToList().AsReadOnly();
+ return _definitions;
+ }
+ }
+
+ private void UpdateToolTypeCache(IReadOnlyCollection toolInfos)
+ {
+ var namesByType = toolInfos
+ .Where(x => !_uncacheableToolTypes.ContainsKey(x.ToolType))
+ .Where(x => !string.IsNullOrWhiteSpace(x.Definition.Name))
+ .GroupBy(x => x.ToolType)
+ .ToDictionary(x => x.Key, x => x.Select(tool => tool.Definition.Name).Distinct(StringComparer.OrdinalIgnoreCase).ToList());
+
+ foreach (var (toolType, toolNames) in namesByType)
+ {
+ if (toolNames.Count != 1)
+ {
+ foreach (var toolName in toolNames)
+ {
+ _uncacheableToolNames[toolName] = true;
+ _toolTypes.TryRemove(toolName, out _);
+ }
+
+ continue;
+ }
+
+ var name = toolNames[0];
+ if (_uncacheableToolNames.ContainsKey(name))
+ continue;
+
+ _toolTypes[name] = toolType;
+ }
+ }
+
+ private IReadOnlyCollection GetToolInfos(IReadOnlyCollection tools)
+ {
+ return tools
+ .Select(CreateToolInfo)
+ .Where(x => x != null)
+ .Select(x => x!)
+ .Where(x => !string.IsNullOrWhiteSpace(x.Definition.Name))
+ .ToList();
+ }
+
+ private ToolInfo? CreateToolInfo(IAITool tool) =>
+ TryGetDefinition(tool, out var definition)
+ ? new ToolInfo(tool.GetType(), tool, definition)
+ : null;
+
+ private bool TryGetDefinition(IAITool tool, out AIToolDefinition definition)
+ {
+ try
+ {
+ definition = tool.Definition;
+ return true;
+ }
+ catch (InvalidOperationException)
+ {
+ _uncacheableToolTypes[tool.GetType()] = true;
+ definition = default!;
+ return false;
+ }
+ }
+
+ private static bool IsVisible(AIToolDefinition definition, AIToolQuery query) =>
+ (query.Mutability == null || definition.Mutability == query.Mutability) &&
+ (query.DangerLevel == null || definition.DangerLevel == query.DangerLevel) &&
+ IsVisibleForAgent(definition, query.Agent) &&
+ IsVisibleForTenant(definition, query.TenantId) &&
+ IsVisibleForActor(definition, query.ActorId) &&
+ HasRequiredPermissions(definition, query.UserPermissions);
+
+ private static bool IsVisibleForAgent(AIToolDefinition definition, string? agent)
+ {
+ if (definition.AgentScopes.Count == 0)
+ return true;
+
+ return !string.IsNullOrWhiteSpace(agent) &&
+ definition.AgentScopes.Contains(agent, StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static bool IsVisibleForTenant(AIToolDefinition definition, string? tenantId)
+ {
+ if (tenantId != null)
+ {
+ if (definition.TenantBehavior == AITenantBehavior.HostScoped)
+ return false;
+
+ if (definition.TenantBehavior == AITenantBehavior.CrossTenantDenied)
+ return definition.TenantIds.Contains(tenantId, StringComparer.OrdinalIgnoreCase);
+
+ return definition.TenantIds.Count == 0 || definition.TenantIds.Contains(tenantId, StringComparer.OrdinalIgnoreCase);
+ }
+
+ return definition.TenantBehavior != AITenantBehavior.CrossTenantDenied && definition.TenantIds.Count == 0;
+ }
+
+ private static bool IsVisibleForActor(AIToolDefinition definition, string? actorId)
+ {
+ return definition.ActorIds.Count == 0 ||
+ (!string.IsNullOrWhiteSpace(actorId) && definition.ActorIds.Contains(actorId, StringComparer.OrdinalIgnoreCase));
+ }
+
+ private static bool HasRequiredPermissions(AIToolDefinition definition, ICollection userPermissions)
+ {
+ if (definition.Permissions.Count == 0)
+ return true;
+
+ var grantedPermissions = userPermissions
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ return grantedPermissions.Contains(PermissionNames.All) || definition.Permissions.All(grantedPermissions.Contains);
+ }
+
+ private class ScopedAITool(IServiceScope scope, IAITool inner, bool disposeInner) : IAITool
+ {
+ private bool _disposed;
+
+ public AIToolDefinition Definition => inner.Definition;
+
+ public async ValueTask ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ return await inner.ExecuteAsync(context, cancellationToken);
+ }
+ finally
+ {
+ Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ _disposed = true;
+ if (disposeInner)
+ inner.Dispose();
+
+ scope.Dispose();
+ }
+ }
+
+ private record ResolvedTool(IAITool Tool, bool DisposeTool)
+ {
+ public void Dispose()
+ {
+ if (DisposeTool)
+ Tool.Dispose();
+ }
+ }
+
+ private record ToolInfo(Type ToolType, IAITool Tool, AIToolDefinition Definition);
+}
diff --git a/src/modules/Elsa.AI.Host/Services/InMemoryAIConversationStore.cs b/src/modules/Elsa.AI.Host/Services/InMemoryAIConversationStore.cs
new file mode 100644
index 000000000..d0f8096c7
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Services/InMemoryAIConversationStore.cs
@@ -0,0 +1,76 @@
+using System.Collections.Concurrent;
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Abstractions.Models;
+
+namespace Elsa.AI.Host.Services;
+
+public class InMemoryAIConversationStore : IAITransientConversationStore
+{
+ private readonly ConcurrentDictionary _conversations = new(StringComparer.OrdinalIgnoreCase);
+
+ public ValueTask FindAsync(string id, CancellationToken cancellationToken = default)
+ {
+ _conversations.TryGetValue(id, out var conversation);
+ if (conversation == null || !IsExpired(conversation))
+ return ValueTask.FromResult(conversation);
+
+ _conversations.TryRemove(id, out _);
+ conversation = null;
+
+ return ValueTask.FromResult(conversation);
+ }
+
+ public ValueTask SaveAsync(AIConversation conversation, CancellationToken cancellationToken = default)
+ {
+ Validate(conversation);
+ PruneExpired();
+ _conversations.AddOrUpdate(
+ conversation.Id,
+ conversation,
+ (_, existing) =>
+ {
+ ValidateOwnership(existing, conversation);
+ return conversation;
+ });
+
+ return ValueTask.CompletedTask;
+ }
+
+ private void PruneExpired()
+ {
+ foreach (var conversation in _conversations.Values.Where(IsExpired))
+ _conversations.TryRemove(conversation.Id, out _);
+ }
+
+ private bool IsExpired(AIConversation conversation)
+ {
+ if (conversation.RetentionMode == AIRetentionMode.Ephemeral)
+ return conversation.Status is AIConversationStatus.Completed or AIConversationStatus.Failed;
+
+ if (conversation.RetentionMode == AIRetentionMode.Durable)
+ return false;
+
+ var expiresAt = conversation.RetentionExpiresAt;
+ return expiresAt.HasValue && expiresAt <= DateTimeOffset.UtcNow;
+ }
+
+ private static void ValidateOwnership(AIConversation existing, AIConversation conversation)
+ {
+ if (!string.Equals(NormalizeTenantId(existing.TenantId), NormalizeTenantId(conversation.TenantId), StringComparison.Ordinal))
+ throw new InvalidOperationException("Cannot overwrite an AI conversation that belongs to another tenant.");
+
+ if (!string.IsNullOrWhiteSpace(existing.UserId) && !string.Equals(existing.UserId, conversation.UserId, StringComparison.Ordinal))
+ throw new InvalidOperationException("Cannot overwrite an AI conversation that belongs to another user.");
+ }
+
+ private static string NormalizeTenantId(string? tenantId) => tenantId ?? "";
+
+ private static void Validate(AIConversation conversation)
+ {
+ if (string.IsNullOrWhiteSpace(conversation.Id))
+ throw new ArgumentException("A conversation ID is required.", nameof(conversation));
+
+ if (string.IsNullOrWhiteSpace(conversation.UserId))
+ throw new ArgumentException("A conversation user ID is required.", nameof(conversation));
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/ShellFeatures/AIFeature.cs b/src/modules/Elsa.AI.Host/ShellFeatures/AIFeature.cs
new file mode 100644
index 000000000..11d283324
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/ShellFeatures/AIFeature.cs
@@ -0,0 +1,51 @@
+using CShells.FastEndpoints.Features;
+using CShells.Features;
+using Elsa.AI.Host.Options;
+using Elsa.Extensions;
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.AI.Host.ShellFeatures;
+
+[ShellFeature(
+ "AI",
+ DisplayName = "AI Host",
+ Description = "Hosts Weaver AI orchestration, governed tools, proposals, and audit services",
+ DependsOn = ["ElsaFastEndpoints"])]
+[UsedImplicitly]
+public class AIFeature : IFastEndpointsShellFeature
+{
+ private static readonly AIHostOptions DefaultOptions = new();
+
+ public bool StreamingEnabled { get; set; } = DefaultOptions.StreamingEnabled;
+ public bool ConversationPersistenceEnabled { get; set; } = DefaultOptions.ConversationPersistenceEnabled;
+ public bool ProposalReviewEnabled { get; set; } = DefaultOptions.ProposalReviewEnabled;
+ public TimeSpan ConversationRetention { get; set; } = DefaultOptions.ConversationRetention;
+ public TimeSpan ReconnectGrace { get; set; } = DefaultOptions.ReconnectGrace;
+ public int MaxToolResultBytes { get; set; } = DefaultOptions.MaxToolResultBytes;
+ public int MaxResolvedContextBytes { get; set; } = DefaultOptions.MaxResolvedContextBytes;
+ public string? DefaultProviderName { get; set; } = DefaultOptions.DefaultProviderName;
+ public ICollection Providers { get; set; } = [];
+ public ICollection Agents { get; set; } = [..DefaultOptions.Agents];
+ public ICollection SupportedAttachmentKinds { get; set; } = [..DefaultOptions.SupportedAttachmentKinds];
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAIHostServices(ConfigureOptions);
+ }
+
+ private void ConfigureOptions(AIHostOptions options)
+ {
+ options.StreamingEnabled = StreamingEnabled;
+ options.ConversationPersistenceEnabled = ConversationPersistenceEnabled;
+ options.ProposalReviewEnabled = ProposalReviewEnabled;
+ options.ConversationRetention = ConversationRetention;
+ options.ReconnectGrace = ReconnectGrace;
+ options.MaxToolResultBytes = MaxToolResultBytes;
+ options.MaxResolvedContextBytes = MaxResolvedContextBytes;
+ options.DefaultProviderName = DefaultProviderName;
+ options.Providers = [..Providers];
+ options.Agents = [..Agents];
+ options.SupportedAttachmentKinds = [..SupportedAttachmentKinds];
+ }
+}
diff --git a/src/modules/Elsa.AI.Host/Streaming/AIStreamEventMapper.cs b/src/modules/Elsa.AI.Host/Streaming/AIStreamEventMapper.cs
new file mode 100644
index 000000000..8398230ef
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Streaming/AIStreamEventMapper.cs
@@ -0,0 +1,17 @@
+using Elsa.AI.Abstractions.Models;
+using System.Text.Json.Nodes;
+
+namespace Elsa.AI.Host.Streaming;
+
+public class AIStreamEventMapper
+{
+ public AIStreamEvent Map(string conversationId, AIProviderEvent providerEvent) =>
+ new()
+ {
+ Type = providerEvent.Type,
+ ConversationId = conversationId,
+ Sequence = providerEvent.Sequence,
+ Timestamp = providerEvent.Timestamp,
+ Data = (JsonObject)providerEvent.Data.DeepClone()
+ };
+}
diff --git a/src/modules/Elsa.AI.Host/Streaming/AIStreamSessionManager.cs b/src/modules/Elsa.AI.Host/Streaming/AIStreamSessionManager.cs
new file mode 100644
index 000000000..68b319f2f
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Streaming/AIStreamSessionManager.cs
@@ -0,0 +1,79 @@
+using System.Collections.Concurrent;
+
+namespace Elsa.AI.Host.Streaming;
+
+public class AIStreamSessionManager
+{
+ private readonly ConcurrentDictionary _reconnectStates = new(StringComparer.OrdinalIgnoreCase);
+
+ public void MarkDisconnected(string conversationId, TimeSpan graceWindow)
+ {
+ var now = DateTimeOffset.UtcNow;
+ _reconnectStates[conversationId] = new ReconnectState(now.Add(graceWindow), false);
+ PruneExpired(now);
+ }
+
+ public bool CanReconnect(string conversationId)
+ {
+ while (true)
+ {
+ var now = DateTimeOffset.UtcNow;
+ PruneExpired(now);
+
+ if (!_reconnectStates.TryGetValue(conversationId, out var state))
+ return false;
+
+ if (state.Deadline < now)
+ {
+ _reconnectStates.TryRemove(conversationId, out _);
+ return false;
+ }
+
+ if (state.IsReserved)
+ return false;
+
+ if (_reconnectStates.TryUpdate(conversationId, state with { IsReserved = true }, state))
+ return true;
+ }
+ }
+
+ public void MarkConnected(string conversationId)
+ {
+ _reconnectStates.TryRemove(conversationId, out _);
+ }
+
+ public void ReleaseReconnect(string conversationId)
+ {
+ while (true)
+ {
+ var now = DateTimeOffset.UtcNow;
+ PruneExpired(now);
+
+ if (!_reconnectStates.TryGetValue(conversationId, out var state))
+ return;
+
+ if (state.Deadline < now)
+ {
+ _reconnectStates.TryRemove(conversationId, out _);
+ return;
+ }
+
+ if (!state.IsReserved)
+ return;
+
+ if (_reconnectStates.TryUpdate(conversationId, state with { IsReserved = false }, state))
+ return;
+ }
+ }
+
+ private void PruneExpired(DateTimeOffset now)
+ {
+ foreach (var (conversationId, state) in _reconnectStates)
+ {
+ if (state.Deadline < now)
+ _reconnectStates.TryRemove(conversationId, out _);
+ }
+ }
+
+ private readonly record struct ReconnectState(DateTimeOffset Deadline, bool IsReserved);
+}
diff --git a/src/modules/Elsa.AI.Host/Usings.cs b/src/modules/Elsa.AI.Host/Usings.cs
new file mode 100644
index 000000000..b3fb2b505
--- /dev/null
+++ b/src/modules/Elsa.AI.Host/Usings.cs
@@ -0,0 +1 @@
+global using System.Text.Json.Nodes;
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/AIDbContext.cs b/src/modules/Elsa.AI.Persistence.EFCore/AIDbContext.cs
new file mode 100644
index 000000000..6b8a70377
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/AIDbContext.cs
@@ -0,0 +1,50 @@
+using Elsa.AI.Persistence.EFCore.Entities;
+
+namespace Elsa.AI.Persistence.EFCore;
+
+public class AIDbContext(DbContextOptions options) : DbContext(options)
+{
+ public DbSet Conversations => Set();
+ public DbSet Proposals => Set();
+ public DbSet AuditRecords => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("AIProposals");
+ entity.HasKey(x => x.Id);
+ entity.HasIndex(x => new { x.TenantId, x.ConversationId });
+ entity.HasIndex(x => x.Status);
+ entity.Property(x => x.WorkflowPayload).IsRequired();
+ entity.Property(x => x.ValidationDiagnostics).IsRequired();
+ entity.Property(x => x.Warnings).IsRequired();
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("AIAuditRecords");
+ entity.HasKey(x => x.Id);
+ entity.HasIndex(x => new { x.TenantId, x.ConversationId });
+ entity.HasIndex(x => x.ActorId);
+ entity.HasIndex(x => new { x.TenantId, x.Timestamp });
+ entity.HasIndex(x => x.ProposalId);
+ entity.HasIndex(x => x.ToolInvocationId);
+ entity.Property(x => x.Type).IsRequired();
+ entity.Property(x => x.Data).IsRequired();
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("AIConversations");
+ entity.HasKey(x => x.Id);
+ entity.HasIndex(x => new { x.TenantId, x.UserId });
+ entity.HasIndex(x => x.Status);
+ entity.HasIndex(x => x.RetentionExpiresAt);
+ entity.Property(x => x.UserId).IsRequired();
+ entity.Property(x => x.Status).IsRequired();
+ entity.Property(x => x.RetentionMode).IsRequired();
+ entity.Property(x => x.Messages).IsRequired();
+ });
+ }
+}
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Elsa.AI.Persistence.EFCore.csproj b/src/modules/Elsa.AI.Persistence.EFCore/Elsa.AI.Persistence.EFCore.csproj
new file mode 100644
index 000000000..1a0e0ce8f
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Elsa.AI.Persistence.EFCore.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+ Provides EF Core persistence for Elsa AI conversations, proposals and audit records.
+
+ elsa module ai persistence efcore
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIAuditRecord.cs b/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIAuditRecord.cs
new file mode 100644
index 000000000..79ab5a06c
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIAuditRecord.cs
@@ -0,0 +1,16 @@
+namespace Elsa.AI.Persistence.EFCore.Entities;
+
+public class AIAuditRecord
+{
+ public string Id { get; set; } = default!;
+ public string? TenantId { get; set; }
+ public string ActorId { get; set; } = default!;
+ public string? ConversationId { get; set; }
+ public string? ProposalId { get; set; }
+ public string? ToolInvocationId { get; set; }
+ public string Type { get; set; } = default!;
+ public DateTimeOffset Timestamp { get; set; }
+ public string? TraceId { get; set; }
+ public string Summary { get; set; } = "";
+ public string Data { get; set; } = "{}";
+}
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIConversationRecord.cs b/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIConversationRecord.cs
new file mode 100644
index 000000000..9a85a76ba
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIConversationRecord.cs
@@ -0,0 +1,16 @@
+namespace Elsa.AI.Persistence.EFCore.Entities;
+
+public class AIConversationRecord
+{
+ public string Id { get; set; } = default!;
+ public string? TenantId { get; set; }
+ public string UserId { get; set; } = default!;
+ public string? Title { get; set; }
+ public string Status { get; set; } = default!;
+ public DateTimeOffset CreatedAt { get; set; }
+ public DateTimeOffset UpdatedAt { get; set; }
+ public string? ProviderSessionId { get; set; }
+ public string RetentionMode { get; set; } = default!;
+ public DateTimeOffset? RetentionExpiresAt { get; set; }
+ public string Messages { get; set; } = "[]";
+}
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIProposalRecord.cs b/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIProposalRecord.cs
new file mode 100644
index 000000000..540fbeb7b
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Entities/AIProposalRecord.cs
@@ -0,0 +1,23 @@
+namespace Elsa.AI.Persistence.EFCore.Entities;
+
+public class AIProposalRecord
+{
+ public string Id { get; set; } = default!;
+ public string? TenantId { get; set; }
+ public string ConversationId { get; set; } = default!;
+ public string Kind { get; set; } = default!;
+ public string Status { get; set; } = default!;
+ public string? BaselineWorkflowDefinitionId { get; set; }
+ public string? BaselineVersionId { get; set; }
+ public string WorkflowPayload { get; set; } = "{}";
+ public string Rationale { get; set; } = "";
+ public string Warnings { get; set; } = "[]";
+ public string ValidationDiagnostics { get; set; } = "[]";
+ public string? GraphDiff { get; set; }
+ public string CreatedBy { get; set; } = default!;
+ public DateTimeOffset CreatedAt { get; set; }
+ public string? ReviewedBy { get; set; }
+ public DateTimeOffset? ReviewedAt { get; set; }
+ public string? AppliedBy { get; set; }
+ public DateTimeOffset? AppliedAt { get; set; }
+}
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.AI.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..89f5aee95
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,34 @@
+using Elsa.AI.Abstractions.Contracts;
+using Elsa.AI.Persistence.EFCore;
+using Elsa.AI.Persistence.EFCore.Services;
+using Elsa.AI.Persistence.EFCore.Stores;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddAIPersistenceStores(this IServiceCollection services, Action? configureDbContext = null)
+ {
+ if (!services.Any(x => x.ServiceType == typeof(DbContextOptions)))
+ {
+ if (configureDbContext == null)
+ throw new InvalidOperationException($"Register {nameof(AIDbContext)} with configured {nameof(DbContextOptions)} before calling {nameof(AddAIPersistenceStores)}, or call {nameof(AddAIPersistenceStores)} with a database provider configuration.");
+ else
+ services.AddDbContext(configureDbContext);
+ }
+
+ services.TryAddScoped();
+
+ services.Replace(ServiceDescriptor.Scoped());
+ services.Replace(ServiceDescriptor.Scoped());
+ services.TryAddEnumerable(ServiceDescriptor.Scoped());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
+
+ return services;
+ }
+}
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Features/AIPersistenceFeature.cs b/src/modules/Elsa.AI.Persistence.EFCore/Features/AIPersistenceFeature.cs
new file mode 100644
index 000000000..4e4f0dd78
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Features/AIPersistenceFeature.cs
@@ -0,0 +1,16 @@
+using Elsa.Extensions;
+using Elsa.Features.Abstractions;
+using Elsa.Features.Services;
+using Microsoft.EntityFrameworkCore;
+
+namespace Elsa.AI.Persistence.EFCore.Features;
+
+public class AIPersistenceFeature(IModule module) : FeatureBase(module)
+{
+ public Action? ConfigureDbContext { get; set; }
+
+ public override void Apply()
+ {
+ Services.AddAIPersistenceStores(ConfigureDbContext);
+ }
+}
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/FodyWeavers.xml b/src/modules/Elsa.AI.Persistence.EFCore/FodyWeavers.xml
new file mode 100644
index 000000000..dfa2f2e5f
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Helpers/ExceptionFilters.cs b/src/modules/Elsa.AI.Persistence.EFCore/Helpers/ExceptionFilters.cs
new file mode 100644
index 000000000..c0dd1805b
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Helpers/ExceptionFilters.cs
@@ -0,0 +1,12 @@
+namespace Elsa.AI.Persistence.EFCore.Helpers;
+
+internal static class ExceptionFilters
+{
+ public static bool IsNonFatal(Exception exception) => !IsFatal(exception) && exception is not OperationCanceledException;
+
+ private static bool IsFatal(Exception exception) =>
+ exception is OutOfMemoryException and not InsufficientMemoryException
+ || exception is StackOverflowException
+ || exception is AccessViolationException
+ || exception.InnerException is not null && IsFatal(exception.InnerException);
+}
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.Designer.cs b/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.Designer.cs
new file mode 100644
index 000000000..dc9b08e71
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.Designer.cs
@@ -0,0 +1,158 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Elsa.AI.Persistence.EFCore.Migrations;
+
+[DbContext(typeof(AIDbContext))]
+[Migration("20260521002000_Initial")]
+partial class Initial
+{
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.16");
+
+ modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b =>
+ {
+ b.Property("Id");
+
+ b.Property("ActorId")
+ .IsRequired();
+
+ b.Property("ConversationId");
+
+ b.Property("Data")
+ .IsRequired();
+
+ b.Property("ProposalId");
+
+ b.Property("Summary")
+ .IsRequired();
+
+ b.Property("TenantId");
+
+ b.Property("Timestamp");
+
+ b.Property("ToolInvocationId");
+
+ b.Property("TraceId");
+
+ b.Property("Type")
+ .IsRequired();
+
+ b.HasKey("Id");
+
+ b.HasIndex("ActorId");
+
+ b.HasIndex("ProposalId");
+
+ b.HasIndex("ToolInvocationId");
+
+ b.HasIndex("TenantId", "ConversationId");
+
+ b.HasIndex("TenantId", "Timestamp");
+
+ b.ToTable("AIAuditRecords");
+ });
+
+ modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b =>
+ {
+ b.Property("Id");
+
+ b.Property("CreatedAt");
+
+ b.Property("Messages")
+ .IsRequired();
+
+ b.Property("ProviderSessionId");
+
+ b.Property("RetentionExpiresAt");
+
+ b.Property("RetentionMode")
+ .IsRequired();
+
+ b.Property("Status")
+ .IsRequired();
+
+ b.Property("TenantId");
+
+ b.Property("Title");
+
+ b.Property("UpdatedAt");
+
+ b.Property("UserId")
+ .IsRequired();
+
+ b.HasKey("Id");
+
+ b.HasIndex("RetentionExpiresAt");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("TenantId", "UserId");
+
+ b.ToTable("AIConversations");
+ });
+
+ modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b =>
+ {
+ b.Property("Id");
+
+ b.Property("AppliedBy");
+
+ b.Property("AppliedAt");
+
+ b.Property("BaselineVersionId");
+
+ b.Property("BaselineWorkflowDefinitionId");
+
+ b.Property("ConversationId")
+ .IsRequired();
+
+ b.Property("CreatedAt");
+
+ b.Property("CreatedBy")
+ .IsRequired();
+
+ b.Property("GraphDiff");
+
+ b.Property("Kind")
+ .IsRequired();
+
+ b.Property("Rationale")
+ .IsRequired();
+
+ b.Property("ReviewedBy");
+
+ b.Property("ReviewedAt");
+
+ b.Property("Status")
+ .IsRequired();
+
+ b.Property("TenantId");
+
+ b.Property("ValidationDiagnostics")
+ .IsRequired();
+
+ b.Property("Warnings")
+ .IsRequired();
+
+ b.Property("WorkflowPayload")
+ .IsRequired();
+
+ b.HasKey("Id");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("TenantId", "ConversationId");
+
+ b.ToTable("AIProposals");
+ });
+#pragma warning restore 612, 618
+ }
+}
diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.cs b/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.cs
new file mode 100644
index 000000000..6053c5cb2
--- /dev/null
+++ b/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.cs
@@ -0,0 +1,138 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Elsa.AI.Persistence.EFCore.Migrations;
+
+public partial class Initial : Migration
+{
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AIAuditRecords",
+ columns: table => new
+ {
+ Id = table.Column(nullable: false),
+ TenantId = table.Column(nullable: true),
+ ActorId = table.Column(nullable: false),
+ ConversationId = table.Column(nullable: true),
+ ProposalId = table.Column(nullable: true),
+ ToolInvocationId = table.Column(nullable: true),
+ Type = table.Column(nullable: false),
+ Timestamp = table.Column(nullable: false),
+ TraceId = table.Column(nullable: true),
+ Summary = table.Column(nullable: false),
+ Data = table.Column(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AIAuditRecords", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AIConversations",
+ columns: table => new
+ {
+ Id = table.Column(nullable: false),
+ TenantId = table.Column(nullable: true),
+ UserId = table.Column(nullable: false),
+ Title = table.Column(nullable: true),
+ Status = table.Column(nullable: false),
+ CreatedAt = table.Column(nullable: false),
+ UpdatedAt = table.Column(nullable: false),
+ ProviderSessionId = table.Column(nullable: true),
+ RetentionMode = table.Column(nullable: false),
+ RetentionExpiresAt = table.Column(nullable: true),
+ Messages = table.Column(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AIConversations", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AIProposals",
+ columns: table => new
+ {
+ Id = table.Column