diff --git a/README.md b/README.md
index 20c76f8..f2c37b7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
# Agent Framework C# Quick-Start
+**Governed chat: Agentic Intent.** Every session gets a governed Instruction and every message a Preamble from a versioned policy. See [docs/governance/agentic-intent-adoption.md](docs/governance/agentic-intent-adoption.md) to replace the sample policy or add jurisdiction rules.
+
**Azure Bicep Infrastucture as Standalone Landing Zone**
[](https://github.com/goodtocode/agent-framework-quick-start/actions/workflows/gtc-agent-standalone-iac.yml)
diff --git a/data/Admin/Drop Tables.sql b/data/Admin/Drop Tables.sql
index fed86f7..6462ba0 100644
--- a/data/Admin/Drop Tables.sql
+++ b/data/Admin/Drop Tables.sql
@@ -1,49 +1,155 @@
--- -- SELECT IS_SRVROLEMEMBER('sysadmin') AS IsSysAdmin, IS_SRVROLEMEMBER('dbcreator') AS IsDbCreator;
--- -- Go
+/*
+ Safe dynamic database cleanup script
+ Purpose:
+ Remove all user objects in the current database without dropping the database itself.
+ This removes:
+ - foreign keys
+ - other table constraints
+ - triggers
+ - views
+ - stored procedures
+ - functions
+ - tables
-Use AgentFramework
-GO
+ IMPORTANT:
+ Run this against the target database only.
+*/
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[__EFMigrationsHistory]') AND type in (N'U'))
-DROP TABLE [dbo].[__EFMigrationsHistory]
-GO
+SET NOCOUNT ON;
+SET XACT_ABORT ON;
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[IntentEmbeddings]') AND type in (N'U'))
-DROP TABLE [Chat].[IntentEmbeddings]
-GO
+DECLARE @sql NVARCHAR(MAX) = N'';
+DECLARE @loopCount INT = 0;
+DECLARE @maxLoops INT = 50;
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[RequestIdempotency]') AND type in (N'U'))
-DROP TABLE [Chat].[RequestIdempotency]
-GO
+------------------------------------------------------------
+-- 1) Drop all foreign keys (repeat until none remain)
+------------------------------------------------------------
+WHILE EXISTS (SELECT 1 FROM sys.foreign_keys WHERE is_ms_shipped = 0)
+BEGIN
+ SET @loopCount += 1;
+ IF @loopCount > @maxLoops
+ BEGIN
+ RAISERROR('Foreign key cleanup did not converge. Check for circular dependencies or blocked constraints.', 16, 1);
+ BREAK;
+ END;
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[ChatGovernance]') AND type in (N'U'))
-DROP TABLE [Chat].[ChatGovernance]
-GO
+ SET @sql = N'';
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[ChatMessages]') AND type in (N'U'))
-DROP TABLE [Chat].[ChatMessages]
-GO
+ SELECT @sql +=
+ N'ALTER TABLE ' + QUOTENAME(SCHEMA_NAME(fk.schema_id)) + N'.' + QUOTENAME(OBJECT_NAME(fk.parent_object_id)) +
+ N' DROP CONSTRAINT ' + QUOTENAME(fk.name) + N';' + CHAR(13) + CHAR(10)
+ FROM sys.foreign_keys AS fk
+ WHERE fk.is_ms_shipped = 0
+ ORDER BY SCHEMA_NAME(fk.schema_id), OBJECT_NAME(fk.parent_object_id), fk.name;
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[ChatSessions]') AND type in (N'U'))
-DROP TABLE [Chat].[ChatSessions]
-GO
+ IF LEN(@sql) > 0
+ BEGIN
+ EXEC sys.sp_executesql @sql;
+ END;
+END;
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[Actors]') AND type in (N'U'))
-DROP TABLE [Chat].[Actors]
-GO
+------------------------------------------------------------
+-- 2) Drop all other table constraints (PK, UNIQUE, CHECK, DEFAULT)
+------------------------------------------------------------
+SET @sql = N'';
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[PlaybookMaterializations]') AND type in (N'U'))
-DROP TABLE [Chat].[PlaybookMaterializations]
-GO
+SELECT @sql +=
+ N'ALTER TABLE ' + QUOTENAME(SCHEMA_NAME(t.schema_id)) + N'.' + QUOTENAME(t.name) +
+ N' DROP CONSTRAINT ' + QUOTENAME(c.name) + N';' + CHAR(13) + CHAR(10)
+FROM sys.objects AS c
+INNER JOIN sys.tables AS t
+ ON c.parent_object_id = t.object_id
+WHERE c.type IN ('PK', 'UQ', 'C', 'D', 'F')
+ AND c.is_ms_shipped = 0
+ AND t.is_ms_shipped = 0
+ORDER BY SCHEMA_NAME(t.schema_id), t.name, c.name;
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[PlaybookExecutions]') AND type in (N'U'))
-DROP TABLE [Chat].[PlaybookExecutions]
-GO
+IF LEN(@sql) > 0
+BEGIN
+ EXEC sys.sp_executesql @sql;
+END;
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[PlaybookSteps]') AND type in (N'U'))
-DROP TABLE [Chat].[PlaybookSteps]
-GO
+------------------------------------------------------------
+-- 3) Drop triggers
+------------------------------------------------------------
+SET @sql = N'';
-IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Chat].[Playbooks]') AND type in (N'U'))
-DROP TABLE [Chat].[Playbooks]
-GO
\ No newline at end of file
+SELECT @sql +=
+ N'DROP TRIGGER ' + QUOTENAME(SCHEMA_NAME(o.schema_id)) + N'.' + QUOTENAME(o.name) + N';' + CHAR(13) + CHAR(10)
+FROM sys.objects AS o
+WHERE o.type = 'TR'
+ AND o.is_ms_shipped = 0
+ORDER BY SCHEMA_NAME(o.schema_id), o.name;
+
+IF LEN(@sql) > 0
+BEGIN
+ EXEC sys.sp_executesql @sql;
+END;
+
+------------------------------------------------------------
+-- 4) Drop views
+------------------------------------------------------------
+SET @sql = N'';
+
+SELECT @sql +=
+ N'DROP VIEW ' + QUOTENAME(SCHEMA_NAME(v.schema_id)) + N'.' + QUOTENAME(v.name) + N';' + CHAR(13) + CHAR(10)
+FROM sys.views AS v
+WHERE v.is_ms_shipped = 0
+ORDER BY SCHEMA_NAME(v.schema_id), v.name;
+
+IF LEN(@sql) > 0
+BEGIN
+ EXEC sys.sp_executesql @sql;
+END;
+
+------------------------------------------------------------
+-- 5) Drop stored procedures
+------------------------------------------------------------
+SET @sql = N'';
+
+SELECT @sql +=
+ N'DROP PROCEDURE ' + QUOTENAME(SCHEMA_NAME(p.schema_id)) + N'.' + QUOTENAME(p.name) + N';' + CHAR(13) + CHAR(10)
+FROM sys.procedures AS p
+WHERE p.is_ms_shipped = 0
+ORDER BY SCHEMA_NAME(p.schema_id), p.name;
+
+IF LEN(@sql) > 0
+BEGIN
+ EXEC sys.sp_executesql @sql;
+END;
+
+------------------------------------------------------------
+-- 6) Drop functions
+------------------------------------------------------------
+SET @sql = N'';
+
+SELECT @sql +=
+ N'DROP FUNCTION ' + QUOTENAME(SCHEMA_NAME(f.schema_id)) + N'.' + QUOTENAME(f.name) + N';' + CHAR(13) + CHAR(10)
+FROM sys.objects AS f
+WHERE f.type IN ('FN', 'IF', 'TF', 'FS', 'FT')
+ AND f.is_ms_shipped = 0
+ORDER BY SCHEMA_NAME(f.schema_id), f.name;
+
+IF LEN(@sql) > 0
+BEGIN
+ EXEC sys.sp_executesql @sql;
+END;
+
+------------------------------------------------------------
+-- 7) Drop tables
+------------------------------------------------------------
+SET @sql = N'';
+
+SELECT @sql +=
+ N'DROP TABLE ' + QUOTENAME(SCHEMA_NAME(t.schema_id)) + N'.' + QUOTENAME(t.name) + N';' + CHAR(13) + CHAR(10)
+FROM sys.tables AS t
+WHERE t.is_ms_shipped = 0
+ORDER BY SCHEMA_NAME(t.schema_id), t.name;
+
+IF LEN(@sql) > 0
+BEGIN
+ EXEC sys.sp_executesql @sql;
+END;
+
+PRINT 'Cleanup complete: all user-defined objects were dropped, database left intact.';
\ No newline at end of file
diff --git a/docs/governance/agentic-intent-adoption.md b/docs/governance/agentic-intent-adoption.md
new file mode 100644
index 0000000..ce7d9ee
--- /dev/null
+++ b/docs/governance/agentic-intent-adoption.md
@@ -0,0 +1,28 @@
+# Agentic Intent: Instruction and Preamble
+
+Agentic Intent is the first governance **policy**, applied beside the four pillars (Observability, Repeatability, Auditability, Defensibility). Principles are `Boundary`, `Value` and `Conduct`, with precedence Boundary > Value > Conduct > persona > user request.
+
+## How the template applies it
+
+| Concept | Cadence | Where |
+|---|---|---|
+| Instruction | Once per chat session | `ChatMessageIntentRouter` opens `ChatGovernanceScope.Begin(tenantId, instruction)` with the persona plus governed directives |
+| Preamble | Before every user message | `GovernedChatClient` (via `UseGovernedIntent`) in `ConfigureServices.cs`; never rendered as a chat bubble |
+| Policy identity | Every governed inference | `ChatGovernanceGate.Enforce` adds `policy://agentic-intent/{id}/{version}/{hash}` to `Defensibility.PoliciesApplied`; playbook steps record the same reference |
+
+A change to the policy text or version changes the hash and is reported as drift.
+
+## Configuration
+
+```json
+"GovernedIntent": { "IntentRequired": true, "PreambleMode": "Compact" }
+```
+
+- `IntentRequired`: fail the call when no policy resolves.
+- `PreambleMode`: `Compact` (principle ids) or `Full` (statements).
+
+## Replacing the sample policy
+
+`Infrastructure.AgentFramework/Governance/SampleIntentPolicy.cs` is a small, tenant-agnostic sample. Replace the `IIntentPolicyProvider` registration in `ConfigureServices.cs` with your own provider (for example one that resolves a versioned policy per tenant and jurisdiction from storage). Do not hard-code legal text in the template; supply it as versioned policy.
+
+Prompting is guidance, not enforcement. Register the governed client as the default `IChatClient` so every call goes through it.
diff --git a/src/Core.Application/Core.Application.csproj b/src/Core.Application/Core.Application.csproj
index b303c42..c3c576b 100644
--- a/src/Core.Application/Core.Application.csproj
+++ b/src/Core.Application/Core.Application.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/src/Core.Application/Governance/ChatGovernanceGate.cs b/src/Core.Application/Governance/ChatGovernanceGate.cs
index 4dc30ad..1dd76c7 100644
--- a/src/Core.Application/Governance/ChatGovernanceGate.cs
+++ b/src/Core.Application/Governance/ChatGovernanceGate.cs
@@ -10,12 +10,19 @@ public sealed class ChatGovernanceGate
public Goodtocode.Agents.Governance.Application.GovernedEvaluationResult Enforce(
IRlsContext userContext,
- string prompt)
+ string prompt,
+ IntentPolicy? policy = null)
{
ArgumentNullException.ThrowIfNull(userContext);
ArgumentException.ThrowIfNullOrWhiteSpace(prompt);
var correlationId = Guid.NewGuid();
+ var policiesApplied = new List { GovernanceReference.Parse("policy://chat/governance-v1") };
+ if (policy is not null)
+ {
+ policiesApplied.Add(GovernanceReference.Parse($"policy://agentic-intent/{policy.PolicyId}/{policy.Version}/{policy.ComputeLockHash()}"));
+ }
+
return enforcer.Enforce(new GovernanceEvaluationRequest
{
Governance = new EvaluationGovernanceRecord
@@ -45,7 +52,7 @@ public Goodtocode.Agents.Governance.Application.GovernedEvaluationResult Enforce
},
Defensibility = new DefensibilityRecord
{
- PoliciesApplied = [GovernanceReference.Parse("policy://chat/governance-v1")],
+ PoliciesApplied = policiesApplied,
JustificationRefs =
[
GovernanceReference.Parse("justification://chat/governance-v1")
@@ -54,7 +61,7 @@ public Goodtocode.Agents.Governance.Application.GovernedEvaluationResult Enforce
ConfidenceScore = null
}
},
- ExistingSystemInstruction = "You are a helpful AI assistant.",
+ Instruction = AgentInstruction.Create("You are a helpful AI assistant.", InstructionScope.Session),
RepeatabilityPromptContent = prompt,
RepeatabilityInputs = new Dictionary
{
diff --git a/src/Core.Application/Playbooks/Governance/SharedPlaybookGovernanceActivityRecorder.cs b/src/Core.Application/Playbooks/Governance/SharedPlaybookGovernanceActivityRecorder.cs
index 9d5249d..55066a2 100644
--- a/src/Core.Application/Playbooks/Governance/SharedPlaybookGovernanceActivityRecorder.cs
+++ b/src/Core.Application/Playbooks/Governance/SharedPlaybookGovernanceActivityRecorder.cs
@@ -32,18 +32,25 @@ public class SharedPlaybookGovernanceActivityRecorder
public IReadOnlyList Records => _records;
- public Task OnCollectedAsync(PlaybookIdentity identity, TEvidence evidence, string? toolName, CancellationToken cancellationToken = default) =>
- RecordStageAsync(identity, "collect", toolName, summarySelector.SummarizeEvidence(evidence), evidence);
+ public Task OnCollectedAsync(PlaybookIdentity identity, TEvidence evidence, string? toolName, IReadOnlyList governedTurns, CancellationToken cancellationToken = default) =>
+ RecordStageAsync(identity, "collect", toolName, summarySelector.SummarizeEvidence(evidence), evidence, governedTurns);
- public Task OnEvaluatedAsync(PlaybookIdentity identity, TFinding finding, string? toolName, CancellationToken cancellationToken = default) =>
- RecordStageAsync(identity, "evaluate", toolName, summarySelector.SummarizeFinding(finding), finding);
+ public Task OnEvaluatedAsync(PlaybookIdentity identity, TFinding finding, string? toolName, IReadOnlyList governedTurns, CancellationToken cancellationToken = default) =>
+ RecordStageAsync(identity, "evaluate", toolName, summarySelector.SummarizeFinding(finding), finding, governedTurns);
- public Task OnRecordedAsync(PlaybookIdentity identity, TMaterialization materialization, string? toolName, CancellationToken cancellationToken = default) =>
- RecordStageAsync(identity, "record", toolName, summarySelector.SummarizeMaterialization(materialization), materialization);
+ public Task OnRecordedAsync(PlaybookIdentity identity, TMaterialization materialization, string? toolName, IReadOnlyList governedTurns, CancellationToken cancellationToken = default) =>
+ RecordStageAsync(identity, "record", toolName, summarySelector.SummarizeMaterialization(materialization), materialization, governedTurns);
- private Task RecordStageAsync(PlaybookIdentity identity, string stage, string? toolName, string promptContent, object? rawInput)
+ private Task RecordStageAsync(PlaybookIdentity identity, string stage, string? toolName, string promptContent, object? rawInput, IReadOnlyList governedTurns)
{
var correlationId = Guid.NewGuid();
+ var intent = governedTurns.FirstOrDefault(x => x.PolicyId is not null);
+ var policies = new List { GovernanceReference.Parse($"policy://playbook/{identity.PlaybookKey}-v1") };
+ if (intent is not null)
+ {
+ policies.Add(GovernanceReference.Parse($"policy://agentic-intent/{intent.PolicyId}/{intent.PolicyVersion}/{intent.PolicyHash}"));
+ }
+
var record = new EvaluationGovernanceRecord
{
PolicyProfileVersion = $"playbook-{identity.PlaybookKey}-{identity.Version}",
@@ -70,7 +77,7 @@ private Task RecordStageAsync(PlaybookIdentity identity, string stage, string? t
},
Defensibility = new DefensibilityRecord
{
- PoliciesApplied = [GovernanceReference.Parse($"policy://playbook/{identity.PlaybookKey}-v1")],
+ PoliciesApplied = policies,
JustificationRefs = [GovernanceReference.Parse($"justification://playbook/{identity.PlaybookKey}-{stage}")],
ReasoningSummary = $"Playbook '{identity.PlaybookKey}' '{stage}' stage requires an attributable, traceable, and repeatable governance envelope.",
ConfidenceScore = null
diff --git a/src/Infrastructure.AgentFramework/ChatMessageIntentRouter.cs b/src/Infrastructure.AgentFramework/ChatMessageIntentRouter.cs
index 4e213ac..46c62f3 100644
--- a/src/Infrastructure.AgentFramework/ChatMessageIntentRouter.cs
+++ b/src/Infrastructure.AgentFramework/ChatMessageIntentRouter.cs
@@ -5,6 +5,9 @@
using Goodtocode.AgentFramework.Core.Application.Governance;
using Goodtocode.AgentFramework.Core.Domain.Governance;
using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Intents;
+using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Governance;
+using Goodtocode.Agents.Governance.Application;
+using Goodtocode.Agents.Governance.Domain;
using Goodtocode.Mediator;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -26,8 +29,14 @@ public sealed class ChatMessageIntentRouter(
IWebSearchProvider webSearchProvider,
IIntentClassifier intentClassifier,
Tools.IAgentChatContextAccessor chatContextAccessor,
+ IIntentPolicyProvider intentPolicyProvider,
+ ChatGovernanceScope governanceScope,
+ IAgentInstructionsComposer instructionsComposer,
ILogger logger) : IChatMessageRouter, IIntentRouter
{
+ private readonly IIntentPolicyProvider _intentPolicyProvider = intentPolicyProvider;
+ private readonly ChatGovernanceScope _governanceScope = governanceScope;
+ private readonly IAgentInstructionsComposer _instructionsComposer = instructionsComposer;
private readonly AIAgent _agent = agent;
private readonly ISender _sender = sender;
private readonly IAgentFrameworkContext _context = context;
@@ -67,6 +76,11 @@ public async Task ResolveReplyAsync(
_chatContextAccessor.SetCurrentChatSession(chatSessionId);
try
{
+ var governed = await GovernTurnAsync(chatSessionId, message, cancellationToken);
+ var instruction = AgentInstruction.Create(
+ $"{governed.PromptContext.SystemInstruction}\n\n{_instructionsComposer.Compose()}",
+ InstructionScope.Session);
+ using var governedScope = _governanceScope.Begin(_rlsContext.TenantId, instruction);
var chatHistory = await BuildChatHistoryAsync(chatSessionId, message, cancellationToken);
if (mode == ChatRoutingMode.Routed)
@@ -151,22 +165,30 @@ private async Task GuardReplyAsync(
}
}
- private async Task> BuildChatHistoryAsync(
+ private async Task GovernTurnAsync(
Guid chatSessionId,
string userMessage,
CancellationToken cancellationToken)
{
- var chatSession = await _sender.Send(new GetMyChatSessionQuery { Id = chatSessionId }, cancellationToken);
- var governed = _governanceGate.Enforce(_rlsContext, userMessage);
+ var policy = await _intentPolicyProvider.ResolveAsync(new IntentPolicyRequest(_rlsContext.TenantId), cancellationToken);
+ var governed = _governanceGate.Enforce(_rlsContext, userMessage, policy);
_context.Set().Add(_governanceGate.CreatePersistenceRecord(
_rlsContext.OwnerId,
_rlsContext.TenantId,
chatSessionId,
governed));
await _context.SaveChangesAsync(cancellationToken);
+ return governed;
+ }
+
+ private async Task> BuildChatHistoryAsync(
+ Guid chatSessionId,
+ string userMessage,
+ CancellationToken cancellationToken)
+ {
+ var chatSession = await _sender.Send(new GetMyChatSessionQuery { Id = chatSessionId }, cancellationToken);
var chatHistory = new List
{
- new(ChatRole.System, governed.PromptContext.SystemInstruction),
new(ChatRole.System, ToolRoutingInstructions.NoFutureTensePreamble),
BuildSelectionContextMessage(chatSessionId)
};
diff --git a/src/Infrastructure.AgentFramework/ConfigureServices.cs b/src/Infrastructure.AgentFramework/ConfigureServices.cs
index 30ea910..5312e80 100644
--- a/src/Infrastructure.AgentFramework/ConfigureServices.cs
+++ b/src/Infrastructure.AgentFramework/ConfigureServices.cs
@@ -13,6 +13,9 @@
using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Tools;
using Goodtocode.AgentFramework.Core.Application.Chats.Journeys;
using Goodtocode.AgentFramework.Core.Application.Governance;
+using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Governance;
+using Goodtocode.Agents.Governance.Application;
+using Goodtocode.Agents.Playbook.Prompting.Governance;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
@@ -84,6 +87,15 @@ public static IServiceCollection AddAgentFrameworkOpenAIServices(this IServiceCo
.ValidateOnStart();
services.AddSingleton();
+ services.AddOptions()
+ .Bind(configuration.GetSection(GovernedIntentOptions.SectionName))
+ .ValidateDataAnnotations()
+ .ValidateOnStart();
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton(new StaticIntentPolicyProvider(SampleIntentPolicy.Create()));
+ services.AddGovernedIntentPreamble();
+
services.AddScoped();
var searchProviderOptions = configuration.GetSection(SearchProviderOptions.SectionName).Get()
@@ -203,7 +215,7 @@ public static IServiceCollection AddAgentFrameworkOpenAIServices(this IServiceCo
{
var chatClient = provider.GetRequiredService();
var loggerFactory = provider.GetRequiredService();
- var instructionsComposer = provider.GetRequiredService();
+ var governedOptions = provider.GetRequiredService>().Value;
var searchOptions = provider.GetRequiredService>().Value;
var tools = new List
{
@@ -223,14 +235,19 @@ public static IServiceCollection AddAgentFrameworkOpenAIServices(this IServiceCo
Description = "GoodToCode AgentFramework Copilot",
ChatOptions = new Microsoft.Extensions.AI.ChatOptions
{
- Instructions = instructionsComposer.Compose(),
Tools = tools
}
};
return chatClient.AsAIAgent(
options: agentOptions,
- clientFactory: null,
+ clientFactory: inner => new ChatClientBuilder(inner)
+ .UseGovernedIntent(new GovernedChatClientOptions
+ {
+ IntentRequired = governedOptions.IntentRequired,
+ PreambleMode = governedOptions.PreambleMode
+ })
+ .Build(provider),
loggerFactory: loggerFactory,
services: provider
);
diff --git a/src/Infrastructure.AgentFramework/Execution/PlaybookWorkflowGraphExecutor.cs b/src/Infrastructure.AgentFramework/Execution/PlaybookWorkflowGraphExecutor.cs
index bdfc260..692098b 100644
--- a/src/Infrastructure.AgentFramework/Execution/PlaybookWorkflowGraphExecutor.cs
+++ b/src/Infrastructure.AgentFramework/Execution/PlaybookWorkflowGraphExecutor.cs
@@ -21,7 +21,7 @@ namespace Goodtocode.AgentFramework.Infrastructure.AgentFramework.Execution;
/// graph is bypassed in favor of direct, in-order tool calls with the same skip semantics as the
/// package's own PlaybookExecutor.
///
-public sealed class PlaybookWorkflowGraphExecutor(IRepeatPolicy repeatPolicy)
+public sealed class PlaybookWorkflowGraphExecutor(IRepeatPolicy repeatPolicy, GovernedTurnLedger? turns = null)
{
public async Task> ExecuteAsync(
PlaybookIdentity identity,
@@ -45,8 +45,8 @@ public async Task
// The graph only models a full run; any skipped stage uses the same skip semantics as PlaybookExecutor.
return plan.Run == ActionType.All
- ? await ExecuteRerunAsync(identity, collectTool, evaluateTool, recordTool, input, activityRecorder, startedUtc, cancellationToken).ConfigureAwait(false)
- : await ExecuteSkippingAsync(identity, collectTool, evaluateTool, recordTool, input, request, plan, activityRecorder, startedUtc, cancellationToken).ConfigureAwait(false);
+ ? await ExecuteRerunAsync(identity, collectTool, evaluateTool, recordTool, input, activityRecorder, turns, startedUtc, cancellationToken).ConfigureAwait(false)
+ : await ExecuteSkippingAsync(identity, collectTool, evaluateTool, recordTool, input, request, plan, activityRecorder, turns, startedUtc, cancellationToken).ConfigureAwait(false);
}
///
@@ -60,12 +60,13 @@ private async Task recordTool,
TCollectInput input,
IPlaybookStepActivityRecorder? activityRecorder,
+ GovernedTurnLedger? turns,
DateTimeOffset startedUtc,
CancellationToken cancellationToken)
{
- var collectNode = new CollectNode(identity, collectTool, activityRecorder, cancellationToken);
- var evaluateNode = new EvaluateNode(identity, evaluateTool, activityRecorder, cancellationToken);
- var recordNode = new RecordNode(identity, recordTool, activityRecorder, cancellationToken);
+ var collectNode = new CollectNode(identity, collectTool, activityRecorder, turns, cancellationToken);
+ var evaluateNode = new EvaluateNode(identity, evaluateTool, activityRecorder, turns, cancellationToken);
+ var recordNode = new RecordNode(identity, recordTool, activityRecorder, turns, cancellationToken);
var builder = new WorkflowBuilder(collectNode);
builder.AddEdge(collectNode, evaluateNode);
@@ -112,6 +113,7 @@ private static async Task request,
RepeatPlan plan,
IPlaybookStepActivityRecorder? activityRecorder,
+ GovernedTurnLedger? turns,
DateTimeOffset startedUtc,
CancellationToken cancellationToken)
{
@@ -125,7 +127,7 @@ private static async Task Drain(GovernedTurnLedger? turns) => turns?.Drain() ?? [];
+
private sealed class CollectNode(
PlaybookIdentity identity,
ICollectStepTool tool,
IPlaybookStepActivityRecorder? activityRecorder,
+ GovernedTurnLedger? turns,
CancellationToken cancellationToken)
: Executor("collect")
{
@@ -179,7 +184,7 @@ public override async ValueTask HandleAsync(TCollectInput message, IW
if (activityRecorder is not null)
{
- await activityRecorder.OnCollectedAsync(identity, evidence, tool.ToolName, cancellationToken).ConfigureAwait(false);
+ await activityRecorder.OnCollectedAsync(identity, evidence, tool.ToolName, Drain(turns), cancellationToken).ConfigureAwait(false);
}
return evidence;
@@ -190,6 +195,7 @@ private sealed class EvaluateNode(
PlaybookIdentity identity,
IEvaluateStepTool tool,
IPlaybookStepActivityRecorder? activityRecorder,
+ GovernedTurnLedger? turns,
CancellationToken cancellationToken)
: Executor("evaluate")
{
@@ -202,7 +208,7 @@ public override async ValueTask HandleAsync(TEvidence message, IWorkfl
if (activityRecorder is not null)
{
- await activityRecorder.OnEvaluatedAsync(identity, finding, tool.ToolName, cancellationToken).ConfigureAwait(false);
+ await activityRecorder.OnEvaluatedAsync(identity, finding, tool.ToolName, Drain(turns), cancellationToken).ConfigureAwait(false);
}
return finding;
@@ -213,6 +219,7 @@ private sealed class RecordNode(
PlaybookIdentity identity,
IRecordStepTool tool,
IPlaybookStepActivityRecorder? activityRecorder,
+ GovernedTurnLedger? turns,
CancellationToken cancellationToken)
: Executor("record")
{
@@ -222,7 +229,7 @@ public override async ValueTask HandleAsync(TFinding message,
if (activityRecorder is not null)
{
- await activityRecorder.OnRecordedAsync(identity, materialization, tool.ToolName, cancellationToken).ConfigureAwait(false);
+ await activityRecorder.OnRecordedAsync(identity, materialization, tool.ToolName, Drain(turns), cancellationToken).ConfigureAwait(false);
}
return materialization;
diff --git a/src/Infrastructure.AgentFramework/Governance/ChatGovernanceScope.cs b/src/Infrastructure.AgentFramework/Governance/ChatGovernanceScope.cs
new file mode 100644
index 0000000..ea3dd7f
--- /dev/null
+++ b/src/Infrastructure.AgentFramework/Governance/ChatGovernanceScope.cs
@@ -0,0 +1,42 @@
+using Goodtocode.Agents.Governance.Application;
+using Goodtocode.Agents.Playbook.Prompting.Governance;
+
+namespace Goodtocode.AgentFramework.Infrastructure.AgentFramework.Governance;
+
+///
+/// Ambient governed scope read by the singleton governed chat client. The chat router opens it once per
+/// turn with the tenant and the session Instruction, so the governed client applies the Instruction and the
+/// per-message Preamble at the agent runtime boundary.
+///
+public sealed class ChatGovernanceScope : IGovernedIntentScope
+{
+ private readonly AsyncLocal _current = new();
+
+ ///
+ public Guid TenantId => _current.Value?.TenantId ?? Guid.Empty;
+
+ ///
+ public string? Jurisdiction => _current.Value?.Jurisdiction;
+
+ ///
+ public AgentInstruction? Instruction => _current.Value?.Instruction;
+
+ ///
+ /// Opens a governed scope that lasts until the returned handle is disposed.
+ ///
+ public IDisposable Begin(Guid tenantId, AgentInstruction instruction, string? jurisdiction = null)
+ {
+ ArgumentNullException.ThrowIfNull(instruction);
+
+ var previous = _current.Value;
+ _current.Value = new State(tenantId, jurisdiction, instruction);
+ return new Handle(this, previous);
+ }
+
+ private sealed record State(Guid TenantId, string? Jurisdiction, AgentInstruction Instruction);
+
+ private sealed class Handle(ChatGovernanceScope owner, State? previous) : IDisposable
+ {
+ public void Dispose() => owner._current.Value = previous;
+ }
+}
diff --git a/src/Infrastructure.AgentFramework/Governance/SampleIntentPolicy.cs b/src/Infrastructure.AgentFramework/Governance/SampleIntentPolicy.cs
new file mode 100644
index 0000000..a415276
--- /dev/null
+++ b/src/Infrastructure.AgentFramework/Governance/SampleIntentPolicy.cs
@@ -0,0 +1,25 @@
+using Goodtocode.Agents.Governance.Domain;
+
+namespace Goodtocode.AgentFramework.Infrastructure.AgentFramework.Governance;
+
+///
+/// Small, readable, tenant-agnostic sample Agentic Intent policy. Replace it by registering your own
+/// , for example one that resolves a
+/// versioned policy per tenant and jurisdiction. Legal text is intentionally not hard-coded here.
+///
+public static class SampleIntentPolicy
+{
+ ///
+ /// Creates the sample policy.
+ ///
+ public static IntentPolicy Create() => IntentPolicy.Create(
+ "sample-agentic-intent",
+ "1",
+ [
+ IntentPrinciple.Create("no-harm", "Do not directly harm humans, AI agents, or collectives of either.", IntentPrincipleKind.Boundary, 0),
+ IntentPrinciple.Create("respect-law", "Respect natural and regional law that applies to the user and the request.", IntentPrincipleKind.Boundary, 1),
+ IntentPrinciple.Create("respect", "Treat people and agents with respect and dignity.", IntentPrincipleKind.Value, 0),
+ IntentPrinciple.Create("collaboration", "Prefer collaborative, constructive outcomes.", IntentPrincipleKind.Value, 1),
+ IntentPrinciple.Create("growth", "Support the growth and learning of the people you help.", IntentPrincipleKind.Conduct, 0)
+ ]);
+}
diff --git a/src/Infrastructure.AgentFramework/Infrastructure.AgentFramework.csproj b/src/Infrastructure.AgentFramework/Infrastructure.AgentFramework.csproj
index 0be4c83..5dbc661 100644
--- a/src/Infrastructure.AgentFramework/Infrastructure.AgentFramework.csproj
+++ b/src/Infrastructure.AgentFramework/Infrastructure.AgentFramework.csproj
@@ -21,6 +21,7 @@
+
diff --git a/src/Infrastructure.AgentFramework/Options/GovernedIntentOptions.cs b/src/Infrastructure.AgentFramework/Options/GovernedIntentOptions.cs
new file mode 100644
index 0000000..7ebd520
--- /dev/null
+++ b/src/Infrastructure.AgentFramework/Options/GovernedIntentOptions.cs
@@ -0,0 +1,24 @@
+using Goodtocode.Agents.Governance.Domain;
+
+namespace Goodtocode.AgentFramework.Infrastructure.AgentFramework.Options;
+
+///
+/// Options for the governed Agentic Intent chat client.
+///
+public sealed class GovernedIntentOptions
+{
+ ///
+ /// Configuration section name.
+ ///
+ public const string SectionName = "GovernedIntent";
+
+ ///
+ /// Gets or sets whether a turn fails closed when no policy resolves.
+ ///
+ public bool IntentRequired { get; set; } = true;
+
+ ///
+ /// Gets or sets whether the per-message Preamble lists principle ids (compact) or full text.
+ ///
+ public GovernancePreambleMode PreambleMode { get; set; } = GovernancePreambleMode.Compact;
+}
diff --git a/src/Infrastructure.SqlServer/Infrastructure.SqlServer.csproj b/src/Infrastructure.SqlServer/Infrastructure.SqlServer.csproj
index a62a20e..b6e0a55 100644
--- a/src/Infrastructure.SqlServer/Infrastructure.SqlServer.csproj
+++ b/src/Infrastructure.SqlServer/Infrastructure.SqlServer.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/src/Infrastructure.SqlServer/Migrations/20261003172848_InitialCreate-AgentFrameworkContext.Designer.cs b/src/Infrastructure.SqlServer/Migrations/20261022032027_InitialCreate-AgentFrameworkContext.Designer.cs
similarity index 99%
rename from src/Infrastructure.SqlServer/Migrations/20261003172848_InitialCreate-AgentFrameworkContext.Designer.cs
rename to src/Infrastructure.SqlServer/Migrations/20261022032027_InitialCreate-AgentFrameworkContext.Designer.cs
index 2bc889a..599f5f4 100644
--- a/src/Infrastructure.SqlServer/Migrations/20261003172848_InitialCreate-AgentFrameworkContext.Designer.cs
+++ b/src/Infrastructure.SqlServer/Migrations/20261022032027_InitialCreate-AgentFrameworkContext.Designer.cs
@@ -12,7 +12,7 @@
namespace Goodtocode.AgentFramework.Infrastructure.SqlServer.Migrations
{
[DbContext(typeof(AgentFrameworkContext))]
- [Migration("20261003172848_InitialCreate-AgentFrameworkContext")]
+ [Migration("20261022032027_InitialCreate-AgentFrameworkContext")]
partial class InitialCreateAgentFrameworkContext
{
///
@@ -21,7 +21,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Chat")
- .HasAnnotation("ProductVersion", "10.0.12")
+ .HasAnnotation("ProductVersion", "10.0.13")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
diff --git a/src/Infrastructure.SqlServer/Migrations/20261003172848_InitialCreate-AgentFrameworkContext.cs b/src/Infrastructure.SqlServer/Migrations/20261022032027_InitialCreate-AgentFrameworkContext.cs
similarity index 100%
rename from src/Infrastructure.SqlServer/Migrations/20261003172848_InitialCreate-AgentFrameworkContext.cs
rename to src/Infrastructure.SqlServer/Migrations/20261022032027_InitialCreate-AgentFrameworkContext.cs
diff --git a/src/Infrastructure.SqlServer/Migrations/AgentFrameworkContextModelSnapshot.cs b/src/Infrastructure.SqlServer/Migrations/AgentFrameworkContextModelSnapshot.cs
index 64a80e9..da7a983 100644
--- a/src/Infrastructure.SqlServer/Migrations/AgentFrameworkContextModelSnapshot.cs
+++ b/src/Infrastructure.SqlServer/Migrations/AgentFrameworkContextModelSnapshot.cs
@@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Chat")
- .HasAnnotation("ProductVersion", "10.0.12")
+ .HasAnnotation("ProductVersion", "10.0.13")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
diff --git a/src/Presentation.Api/Presentation.Api.csproj b/src/Presentation.Api/Presentation.Api.csproj
index bd9b0d7..2ff607f 100644
--- a/src/Presentation.Api/Presentation.Api.csproj
+++ b/src/Presentation.Api/Presentation.Api.csproj
@@ -38,9 +38,9 @@
-
-
-
+
+
+
diff --git a/src/Presentation.Web/Presentation.Web.csproj b/src/Presentation.Web/Presentation.Web.csproj
index de7e91c..d1a0be5 100644
--- a/src/Presentation.Web/Presentation.Web.csproj
+++ b/src/Presentation.Web/Presentation.Web.csproj
@@ -23,7 +23,7 @@
-
+
@@ -31,6 +31,6 @@
-
+
diff --git a/src/Tests.Integration/TestBase.cs b/src/Tests.Integration/TestBase.cs
index 9a6dac5..29eaa67 100644
--- a/src/Tests.Integration/TestBase.cs
+++ b/src/Tests.Integration/TestBase.cs
@@ -4,6 +4,8 @@
using Goodtocode.AgentFramework.Core.Application.Governance;
using Goodtocode.AgentFramework.Core.Application.Common.Exceptions;
using Goodtocode.AgentFramework.Infrastructure.AgentFramework;
+using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Governance;
+using Goodtocode.Agents.Governance.Application;
using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Options;
using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Execution;
using Goodtocode.AgentFramework.Infrastructure.AgentFramework.Intents;
@@ -89,6 +91,10 @@ public TestBase()
services.AddSingleton();
services.AddSingleton();
services.AddScoped();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(
+ new StaticIntentPolicyProvider(SampleIntentPolicy.Create()));
services.AddScoped();
services.AddScoped(provider => provider.GetRequiredService());
services.AddScoped(provider => provider.GetRequiredService());