Patterns & Technologies — The Deep Dive
Every architectural pattern, AI/RAG subsystem, governance behaviour, framework, NuGet and npm dependency the harness ships with — catalogued in one page, cross-linked to the file that implements it. Twelve marquee patterns get a code block; the rest get a sentence and a path. This is the page you scroll once and bookmark.
The onboarding guide has a focused six-pattern primer at Patterns You'll Use Daily. Read that first if you're new — then come back here for the encyclopedic version.
How to read this page
Every entry follows the same shape — pattern name, one or two sentences of "what it is and how the harness uses it", and a file path you can paste into your editor. The twelve patterns you'll meet most often have a code block underneath. Use the table of contents on the right to jump.
1 · Architectural patterns
The bones. These are the patterns that recur in nearly every file — internalize them and the rest of the catalogue becomes a remix of these primitives.
Clean Architecture (four layers)
Domain → Application → Infrastructure → Presentation with strict inward-only dependencies. Domain types are pure C# records; Application defines interfaces and CQRS handlers; Infrastructure implements interfaces against EF Core / Azure / graph backends; Presentation hosts the API, SignalR hub, console UI, and the React dashboard.
src/Content/Domain/— entities, value objects, domain rulessrc/Content/Application/— interfaces, CQRS, MediatR behaviourssrc/Content/Infrastructure/— EF Core, AI clients, graph backends, observabilitysrc/Content/Presentation/— AgentHub (ASP.NET Core), Dashboard (React), ConsoleUI, LoggerUI
CQRS with MediatR (marquee pattern · 1 of 12)
Every command and query flows through MediatR. Handlers stay focused on their one job; cross-cutting concerns are pulled into pipeline behaviours that wrap the handler in registration order. The ordering matters — early behaviours run outermost, so a behaviour registered first sees the request first and the response last.
public sealed class MyBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
// pre-process — can short-circuit by returning early
var response = await next(); // call inner behaviour / handler
// post-process — can transform the response
return response;
}
}
The behaviours registered by the harness, in execution order (outermost first):
UnhandledExceptionBehavior— top-level safety net, structured log + rethrowAmbientRequestScopeBehavior— establishes the ambient request scopeAgentContextPropagationBehavior— copies agent/conversation IDs into ambient OTel baggageAgentIdentityResolutionBehavior— resolves the acting agent identityAuditTrailBehavior— JSONL audit log of every commandContentSafetyBehavior— runs Azure Content Safety (or mock) on inputs & outputsPromptInjectionBehavior— guards against user-prompted system-prompt overridesTokenBudgetBehavior— enforces per-conversation token spendHookBehavior— fires pre/post hooks declared by pluginsRetrievalAuditBehavior— captures RAG retrievals for evaluation replayResponseSanitizationBehavior— masks PII/secrets before returning to client (post-execution)ToolOutputCompressionBehavior— compresses large tool outputs by content type (post-execution)KnowledgeExtractionBehavior— extracts facts from conversation into the knowledge graph (post-turn, fire-and-forget)WorkEpisodeCaptureBehavior— captures work episodes for self-improving memory (post-turn, fire-and-forget)PromptUsageTrackingBehavior— records prompt cache hit/miss for cost analysis
All behaviour files live under src/Content/Application/Application.AI.Common/MediatRBehaviors/.
Note: tool authorization and governance are not MediatR
behaviours. They run on the live tool-execution path — every agent tool is wrapped by
GovernedAIFunction, whose InvokeCoreAsync runs three ambient gates
(IToolInvocationGovernor → IToolClassificationGate →
IProgressEvaluator) before the tool runs. Two earlier behaviours,
GovernancePolicyBehavior and ToolPermissionBehavior, were removed as
dead code because they keyed on a request marker nothing in production implemented.
Result<T> (marquee pattern · 2 of 12)
Expected failures (validation, missing records, business-rule rejections) return
Result<T> rather than throwing. Exceptions are reserved for truly exceptional
conditions — bad config at startup, infrastructure outages, programming errors.
public sealed record Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? Error { get; }
public IReadOnlyList<string> ValidationErrors { get; }
public static Result<T> Success(T value) => new(true, value, null, []);
public static Result<T> Fail(string error) => new(false, default, error, []);
public static Result<T> ValidationFailure(IReadOnlyList<string> errors) => new(false, default, null, errors);
}
Factory pattern (marquee pattern · 3 of 12)
When constructing a service needs multiple dependencies, config lookups, or decoration steps, the harness uses a factory instead of expanding everyone's constructor. The big three:
AgentFactoryAIAgent — loads skills, resolves tools, wires content safety and OTel, applies function-invocation limits. src/Content/Application/Application.AI.Common/Factories/AgentFactory.csChatClientFactoryIChatClient per ClientType — Azure OpenAI, OpenAI, AI Foundry, OpenRouter, Azure AI Inference. Decorates with retries, timeouts, observability, content safety. Split into a Providers partial. src/Content/Infrastructure/Infrastructure.AI/Factories/ChatClientFactory.csAgentExecutionContextFactorysrc/Content/Application/Application.AI.Common/Factories/AgentExecutionContextFactory.cspublic IChatClient Create(ClientType type, ChatClientOptions opts)
{
IChatClient inner = type switch
{
ClientType.AzureOpenAI => BuildAzureOpenAI(opts),
ClientType.OpenAI => BuildOpenAI(opts),
ClientType.AzureAIInference => BuildAzureAIInference(opts),
ClientType.OpenRouter => BuildOpenRouter(opts),
ClientType.Echo => new EchoChatClient(), // tests
_ => throw new InvalidOperationException($"Unknown client type {type}")
};
// every client gets the same decoration stack — order matters
return inner
.WithResilience(_resilienceBuilder) // Polly retries + circuit breaker
.WithContentSafety(_contentSafety) // input/output moderation
.WithUsageTracking(_usageMetrics) // OTel token counters
.WithLogging(_logger); // structured request/response log
}
Never construct an AIAgent, IChatClient, or
AgentExecutionContext directly. Always go through the
factory. Otherwise you lose content safety, OTel, function limits, and config
consistency — and these are exactly the things tests catch you forgetting.
Strategy via keyed DI (marquee pattern · 4 of 12)
Whenever the harness has multiple implementations of the same interface that need to coexist — rerankers, chunkers, graph backends, sandbox executors, chat clients, audit sinks — it registers them with a string key and resolves them lazily.
// registration (DependencyInjection.cs)
services.AddKeyedSingleton<IReranker, AzureSemanticReranker>("AzureSemantic");
services.AddKeyedSingleton<IReranker, CrossEncoderReranker>("CrossEncoder");
services.AddKeyedSingleton<IReranker, NoOpReranker>("NoOp");
// resolution (at runtime, based on config)
var rerankerKey = _cfg.CurrentValue.Rag.Reranker; // e.g. "AzureSemantic"
var reranker = _sp.GetRequiredKeyedService<IReranker>(rerankerKey);
Used throughout the harness for:
- Tools — keyed by tool name (
"file_system","calculation_engine") - Chat clients — keyed by
ClientType - Rerankers —
AzureSemantic,CrossEncoder,NoOp - Chunking strategies —
StructureAware,FixedSize,Semantic - Graph backends —
Neo4j,Kuzu,PostgreSql,InMemory - Sandbox executors —
Process,Docker - Plan step executors — keyed by
StepTypeenum - Compaction strategies —
Full,Partial,Micro - Prompt section providers — keyed by section name
Decorator (marquee pattern · 5 of 12)
Cross-cutting concerns that can't be expressed as MediatR behaviours — because they apply to
long-lived service instances rather than per-request flows — are layered via decorators. The
canonical case is ResilientChatClient, which wraps a raw IChatClient
with a Polly pipeline (retry → circuit breaker → timeout → fallback to next provider).
public sealed class ResilientChatClient(IChatClient inner, ResiliencePipeline pipeline) : IChatClient
{
public Task<ChatResponse> GetResponseAsync(IList<ChatMessage> messages, ChatOptions? opts, CancellationToken ct)
=> pipeline.ExecuteAsync(async token => await inner.GetResponseAsync(messages, opts, token), ct).AsTask();
// streaming + dispose pass through to inner ...
}
Other decorator instances in the harness:
ComplianceAwareGraphStorewraps anyIGraphStorewith retention enforcement & erasure orchestrationTenantIsolatedGraphStorewraps a graph store with scope-validation callsMarkdownCheckpointDecoratorwraps the JSON checkpoint state manager with a Markdown side-write for humansCompositeStateManagerwraps multiple state managers behind a single interface
Composite (DAG plans, marquee pattern · 6 of 12)
A Plan is a directed acyclic graph of PlanStep nodes — every node has
a StepType (LlmCall, ToolUse, HumanGate,
ConditionalBranch, SubPlanInvocation, RetrievalPlan) and
zero or more inbound dependency edges. PlanExecutor walks the DAG topologically
with bounded concurrency.
// PlanExecutor.Scheduling.cs — bounded-concurrency walk of the DAG
while (!queue.IsEmpty)
{
var batch = queue.PopReady(maxConcurrency: _opts.MaxParallelSteps);
var tasks = batch.Select(step =>
{
var executor = _stepExecutors.GetRequiredKeyedService<IPlanStepExecutor>(step.Type);
return executor.ExecuteAsync(step, ctx, ct);
});
await Task.WhenAll(tasks);
await _stateStore.CheckpointAsync(plan, ct); // resume-safe between batches
}
- Core:
src/Content/Infrastructure/Infrastructure.AI/Planner/PlanExecutor.cs - Partials:
.Scheduling.cs,.Recovery.cs,.Summary.cs - Step executors:
Planner/StepExecutors/*.cs— one file perStepType - State store:
Planner/EfCorePlanStateStore.cs(+.Reads.cs,.Mappers.cs)
Options pattern with IOptionsMonitor (marquee pattern · 7 of 12)
All configuration is strongly typed (AppConfig, RagConfig,
SandboxConfig, etc.) and injected via IOptionsMonitor<T> rather
than raw IConfiguration. IOptionsMonitor picks up
appsettings.json changes without restart; IOptions snapshots once.
public class MyService(IOptionsMonitor<AppConfig> cfg) : IMyService
{
private AppConfig Cfg => cfg.CurrentValue; // resolves fresh each call
public void DoThing()
{
var budget = Cfg.Agent.DefaultTokenBudget; // use locally; don't cache across methods
// ...
}
}
Partial class splitting
When a class genuinely needs to be large (the executor for a DAG of step types; the EF Core state store with reads, writes, and mappers; the Kuzu backend with CRUD and community queries), the harness splits it across partial files by responsibility — never by line count alone.
PlanExecutor.cs+.Scheduling.cs+.Recovery.cs+.Summary.csEfCorePlanStateStore.cs+.Reads.cs+.Mappers.csKuzuGraphBackend.cs+.Crud.cs+.Community.csLeidenCommunityDetector.cs+.Algorithm.cs+.GraphConstruction.csChatClientFactory.cs+.Providers.csRagOrchestrator.cs+.MultiHop.csDependencyInjection.cs+.Ingestion.cs,.Retrieval.cs,.GraphRag.cs,.Evaluation.cs(Infrastructure.AI.RAG)
2 · AI / agent patterns
The patterns that make this a harness rather than "an app that calls an LLM." Skills, plugins, tools, manifests, the conversation cache — the structures that let multiple agents, each with their own capability surface, share one runtime.
Skills system — dual mode & prerequisites (marquee pattern · 8 of 12)
Skills are Markdown files (SKILL.md) that declare instructions, tool dependencies,
and optional prerequisites. Agents are multi-skill by default — they compose
instructions and tools from every skill loaded for the turn. Skills run in one of two modes:
---
name: file_navigation
description: Read files, list directories, search by glob
mode: Managed
prerequisites: []
tools:
- file_system
- glob_search
---
You can read and search the filesystem. Prefer `glob_search` when you don't
know the exact filename. Cite file paths in `path:line` form.
- Resolver:
src/Content/Application/Application.AI.Common/Services/Skills/SkillPrerequisiteResolver.cs - Completion tracker:
.../Services/Skills/InMemorySkillCompletionTracker.cs - Content providers:
Infrastructure.AI/Skills/FileSystemSkillContentProvider.cs,CandidateSkillContentProvider.cs - Metadata:
Infrastructure.AI/Skills/SkillMetadataRegistry.cs - Graph amendment:
Infrastructure.AI.KnowledgeGraph/Skills/GraphSkillAmendmentProvider.cs(skills updated by learnings)
Plugin system & boundary governance
Plugins are filesystem directories with a plugin.json manifest declaring the skills
the plugin contributes, any MCP servers it brings, and a governance block
(AllowedTools, DeniedTools, AutonomyLevel). The
PluginPermissionRuleProvider turns those declarations into MediatR rules — and
DeniedTools is bypass-immune: it can't be overridden by auto-approve modes.
- Manifest reader:
Infrastructure.AI/Plugins/PluginManifestReader.cs(verify path — see "Locating plugin internals" note below) - Permissions:
Application.Core/Permissions/PluginPermissionRuleProvider.cs - Declaration types:
Domain.Common/Config/AI/Plugins/PluginDeclaration.cs
Agent manifest (AGENT.md)
Agents declare themselves in AGENT.md — name, system-prompt fragments, the skills
they load, the MCP servers they connect to, the decision frameworks they follow. The
AgentMetadataRegistry parses these at startup; the AgentFactory
consumes them at turn-construction time.
Tool registration via keyed DI
Tools register as AddKeyedSingleton<ITool>("name", impl). The skill declares
what it needs by name; the harness resolves at execution time. Different agents can mount
different implementations under the same key — the LLM only ever sees the schema, never the
construction details.
- Implementations:
Infrastructure.AI/Tools/*.cs(FileSystemTool, ReadHistoryTool, DocumentIngestTool, EchoCalculateTool, EchoLookupTool, ...) - Schema generation: tools expose
JsonSchemavia Microsoft.Extensions.AI'sAIToolconversion
Tool output compression
Large tool outputs flood context windows fast. ToolOutputCompressionBehavior
detects content type (JSON, plain text, log lines, structured table) and applies a type-specific
strategy: drop deep nesting, summarise repetitive rows, gzip-and-pointer for truly large blobs.
src/Content/Application/Application.AI.Common/MediatRBehaviors/ToolOutputCompressionBehavior.cs
Content safety middleware
Both input prompts and model outputs flow through ContentSafetyBehavior — Azure AI
Content Safety in production, a structured-log fake in dev (StructuredLogContentSafetyService).
Severity thresholds are per-category and configurable.
Conversation cache
AgentConversationCache keeps reconstructed ChatClientAgent instances
warm per (conversationId, skillIds). Cuts LLM-state reconstruction overhead between
turns; background sync to a persistent store survives restarts.
src/Content/Application/Application.AI.Common/Services/AgentConversationCache.cs
Agent execution context
A scoped AgentExecutionContext rides every turn — agent ID, conversation ID,
loaded skills, resolved tools, OTel session, ambient cancellation token. Behaviours and step
executors pull from it instead of threading 12 parameters everywhere.
src/Content/Domain/Domain.AI/Agents/AgentExecutionContext.cs
Prompt composition & caching
System prompts are assembled section-by-section through keyed IPromptSectionProvider
implementations (identity, permission rules, session state, tool schemas), memoized by
MemoizedPromptComposer, and cache-tracked via Sha256PromptCacheTracker
so the harness can measure how often Anthropic / OpenAI prompt caching actually hits.
Infrastructure.AI/Prompts/Sections/AgentIdentitySectionProvider.cs.../Sections/PermissionRulesSectionProvider.cs.../Sections/SessionStateSectionProvider.cs.../Sections/ToolSchemasSectionProvider.cs.../MemoizedPromptComposer.cs+InMemoryPromptSectionCache.cs
Context compaction
When a conversation approaches the model's context window, ContextCompactionService
invokes one of three strategies keyed by state: Full (rewrite the whole transcript),
Partial (summarise older turns), or Micro (drop redundant tool
outputs). AutoCompactStateMachine picks the right strategy.
On the live agent turn, compaction runs as a chat-client middleware,
ContextCompactionMiddleware (Application.AI.Common/Middleware/ContextCompactionMiddleware.cs).
Before each model call it estimates the incoming history's token footprint and, once it exceeds
MiddlewareMaxContextTokens (default 128 000) scaled by
AutoCompactThresholdRatio (default 0.85), summarises the older history and rebuilds
the request as [system summary] + [current turn].
Opt-in, default OFF. The middleware is wired by AgentFactory only
when AppConfig:AI:ContextManagement:Compaction:MiddlewareEnabled is true
and an IContextCompactionService is registered; otherwise the pipeline omits
it entirely and history is never compacted on the live path (legacy behaviour preserved). It is
also fail-open: if the compaction service fails (LLM summariser unavailable,
circuit breaker open), the original untrimmed history is forwarded unchanged — a compaction
problem never breaks a live turn.
Known limitation. The Full strategy sends the entire transcript to
the LLM on every triggering turn with no cross-turn caching of the summary, so a long
conversation that stays over budget re-summarises repeatedly and pays the summarisation cost each
turn. Prefer Partial/Micro for cost-sensitive long sessions until
summary caching is added.
Subagent dispatch & mailbox
Agents can dispatch subagents for parallel sub-tasks. SubagentToolResolver
exposes a subagent as a callable tool to its parent; InMemoryAgentMailbox routes
messages; BuiltInSubagentProfiles ships ready-made profiles (planner, architect,
explorer, ...). Delegation transcripts persist via JsonlDelegationStore.
A2A hosting
A2AAgentHost exposes an agent over the Agent-to-Agent protocol — peer agents from
other systems can call this harness's agents as ordinary remote endpoints.
src/Content/Infrastructure/Infrastructure.AI/A2A/A2AAgentHost.cs
3 · RAG (Retrieval-Augmented Generation) patterns
A full pipeline: ingest → chunk → enrich → embed → retrieve (dense + sparse + graph + SQL + web)
→ rank → evaluate → assemble. Every stage has a swappable implementation and an evaluation
hook. Live under src/Content/Infrastructure/Infrastructure.AI.RAG/.
Chunking — three strategies
StructureAwareChunker— respects Markdown headings, code fences, list boundaries (Ingestion/StructureAwareChunker.cs)FixedSizeChunker— token-count splits with configurable overlap (Ingestion/FixedSizeChunker.cs)SemanticChunker— cluster adjacent sentences by embedding similarity, split at low-similarity boundaries (Ingestion/SemanticChunker.cs)- Resolver:
Ingestion/ChunkingStrategyResolver.cs
Contextual chunk enrichment
Implements the Anthropic "contextual retrieval" pattern: before embedding, prepend each chunk
with a short LLM-generated description of how the chunk relates to the parent document. Boosts
recall on ambiguous queries.
Ingestion/ContextualChunkEnricher.cs
RAPTOR hierarchical summarisation
Recursive Abstractive Processing for Tree-Organized Retrieval — leaf chunks summarise upward
into intermediate nodes, then a root. Retrieval can target any level: the leaves for citations,
the root for "what is this whole document about?".
Ingestion/RaptorSummarizer.cs
Hybrid retrieval + Reciprocal Rank Fusion (marquee pattern · 9 of 12)
Dense (embedding) and sparse (BM25) retrievals run in parallel; their ranked result lists fuse
via Reciprocal Rank Fusion, which scores each candidate by the sum of 1 / (k + rank)
across all source lists. Captures both semantic and keyword matches without trusting either
one alone.
var (dense, sparse) = await Task.WhenAll(
_vectorStore.SearchAsync(queryEmbedding, k: _opts.DenseK, ct),
_bm25Store.SearchAsync(query, k: _opts.SparseK, ct));
// RRF: each candidate gets sum of 1 / (k + rank_in_list) across lists
var fused = ReciprocalRankFusion.Fuse(
new[] { dense.Results, sparse.Results },
k: _opts.RrfK);
return fused.Take(_opts.FinalK).ToImmutableArray();
- Hybrid wrapper:
Retrieval/HybridRetriever.cs - Iterative variant (multi-hop):
Retrieval/IterativeRetriever.cs - Feedback-weighted scorer:
Retrieval/FeedbackWeightedScorer.cs
Query transformation
RagFusionTransformer— generates N paraphrases of the query, retrieves with each, fuses results (QueryTransform/RagFusionTransformer.cs)HydeTransformer— generates a hypothetical answer, embeds that as the search vector (QueryTransform/HydeTransformer.cs)QueryDecomposer— breaks complex queries into sub-questions for multi-hop (QueryTransform/QueryDecomposer.cs)LlmQueryClassifier— labels query as simple / moderate / complex (QueryTransform/LlmQueryClassifier.cs)QueryRouter— routes by classification to the cheapest pipeline that can handle it (QueryTransform/QueryRouter.cs)
Rerankers (keyed strategy)
AzureSemanticReranker— Azure AI Search's L2 semantic reranker (Retrieval/AzureSemanticReranker.cs)CrossEncoderReranker— transformer-based pairwise scoring (Retrieval/CrossEncoderReranker.cs)NoOpReranker— identity, for benchmarking (Retrieval/NoOpReranker.cs)
Vector & BM25 stores
- Dense:
FaissVectorStore.cs(local),AzureAISearchVectorStore.cs(managed) - Sparse:
SqliteFts5Store.cs(local),AzureAISearchBm25Store.cs(managed) - Factory:
Retrieval/VectorStoreFactory.cs
CRAG (Corrective RAG) evaluation (marquee pattern · 10 of 12)
Every retrieval is scored for sufficiency before assembly. The CragEvaluator returns
one of Accept (chunks are good enough — assemble), Refine
(decent but rerun with a transformed query), or Reject (none of these chunks
help — fall through to web search or escalate). Thresholds are configurable.
var quality = await _retrievalQualityEvaluator.ScoreAsync(query, chunks, ct);
return quality.MaxScore switch
{
var s when s >= _opts.AcceptThreshold => CragDecision.Accept(chunks),
var s when s >= _opts.RefineThreshold => CragDecision.Refine(chunks, suggestedQuery),
_ => CragDecision.Reject(reason: quality.RejectionReason)
};
- Evaluator:
Evaluation/CragEvaluator.cs - Sufficiency:
Evaluation/SufficiencyEvaluator.cs(used by multi-hop "do we have enough yet?" loop) - Retrieval quality:
Evaluation/RetrievalQualityEvaluator.cs - Faithfulness:
Evaluation/AnswerFaithfulnessEvaluator.cs(detects hallucination by scoring answer against source chunks)
Assembly — token budget, pointer expansion, citations
RagContextAssembler— packs chunks into the context window under a token budget, attaches citation IDs (Assembly/RagContextAssembler.cs)PointerChunkExpander— when a chunk is selected, optionally pull in its sibling or parent chunks for surrounding context (Assembly/PointerChunkExpander.cs)CitationTracker— maintains chunk → source mapping so the model can cite faithfully (Assembly/CitationTracker.cs)
Multi-source orchestration
MultiSourceOrchestrator runs vector, BM25, graph, SQL, and web retrievals in
parallel, scored against a per-source cost budget. RetrievalDecisionGate applies
quality gates at each stage; RetrievalCostTracker records the spend.
Orchestration/RagOrchestrator.cs(+.MultiHop.cspartial)Orchestration/MultiSourceOrchestrator.csOrchestration/VectorRetrievalSource.csOrchestration/GraphRetrievalSource.csSqlDatabase/SqlDatabaseRetrievalSource.csWebSearch/WebSearchRetrievalSource.cs(Bing-backed by default)Orchestration/RetrievalDecisionGate.csOrchestration/RetrievalCostTracker.cs
Text-to-SQL retrieval
Structured-data retrieval: TextToSqlGenerator turns a natural-language question
into SQL, JsonSqlQueryTemplateStore + SqlQueryTemplateMatcher match
against curated templates first, and SafeSqlQueryExecutor runs the result against
a read-only connection with parameter binding.
4 · Knowledge graph patterns
A production-grade graph layer, inspired by Cognee — entity extraction, community detection,
feedback learning, cross-session memory, provenance, compliance, and multi-tenant isolation.
Lives under src/Content/Infrastructure/Infrastructure.AI.KnowledgeGraph/ with a
sibling Infrastructure.AI.RAG/GraphRag/ for the GraphRAG-specific code.
Graph backends — four implementations
Neo4jGraphStore— production graph DB via Neo4j driver (Neo4j/Neo4jGraphStore.cs)KuzuGraphBackend— embedded analytical graph DB; split into.Crud.cs+.Community.cspartials (Infrastructure.AI.RAG/GraphRag/KuzuGraphBackend.cs)PostgreSqlGraphStore— relational backend with graph extensions (PostgreSql/PostgreSqlGraphStore.cs)InMemoryGraphStore— fast dev/test backend (InMemory/InMemoryGraphStore.cs)
Leiden community detection
Partitions the graph into communities for hierarchical retrieval. The implementation is split across three partials by concern — the algorithm itself, graph construction, and the public detector interface.
Infrastructure.AI.RAG/GraphRag/LeidenCommunityDetector.cs.../LeidenCommunityDetector.Algorithm.cs.../LeidenCommunityDetector.GraphConstruction.cs
Feedback-weighted search
Every retrieval gets a quality score; GraphFeedbackStore persists it on the
nodes/edges that participated. Future retrievals blend semantic relevance with these historical
weights — chunks that have helped before float upward; chunks that misled get pushed down.
LlmFeedbackDetector derives the quality score from the conversation turn.
Feedback/GraphFeedbackStore.csFeedback/LlmFeedbackDetector.cs- Scorer:
Infrastructure.AI.RAG/Retrieval/FeedbackWeightedScorer.cs
Cross-session knowledge persistence
Four verbs: Remember, Recall, Forget, Improve.
KnowledgeMemoryService coordinates fast reads from InMemorySessionCache
with background sync to the persistent CrossSessionMemoryStore.
ConversationFactExtractor mines facts from each conversation for storage.
Memory/KnowledgeMemoryService.csMemory/InMemorySessionCache.csMemory/ConversationFactExtractor.csInfrastructure.AI.RAG/GraphRag/CrossSessionMemoryStore.cs
Harmonic memory — abstraction + cue-anchor recall
A representation layer over cross-session memory, modeled on Microsoft Research's
Memora. Instead of remembering only the raw fact, each trusted fact also gets a
lightweight scaffolding: a primary abstraction (a canonical "what is
this memory about" summary) and one to three cue anchors (short
[Entity] + [Aspect] phrases). Facts that share a cue anchor form an implicit graph,
so related memories cluster without any explicit edge-building. The raw value is always kept
verbatim — the scaffolding is indexed over it, never a lossy replacement.
On write (Full mode) the service can consolidate: a new fact that
matches a similar existing entry adopts that entry's abstraction so the two share one
topic — a logical topic-adoption, not a physical merge, so every fact keeps its own key, content,
and trust marker. On read, the query is matched against the abstractions and cue
anchors, the shared-anchor cluster around the best hits is pulled in, and that ranking is fused
with the legacy substring/graph recall via Reciprocal Rank Fusion. Matching is
lexical — no LLM on the recall path. The whole layer is a graduated, off-by-default toggle
(Off / AbstractOnly / Full under
AppConfig:AI:HarmonicMemory) because abstraction costs one to two LLM calls per
write; Off is the byte-identical legacy path. The abstractor and consolidator ship as
NotConfigured seams — the consumer supplies the agent-backed implementations.
Memory/KnowledgeMemoryService.Harmonic.cs— write path (abstraction + consolidation)Memory/KnowledgeMemoryService.Harmonic.Recall.cs— cue-anchor recall + RRF fusionDomain.AI/Retrieval/ReciprocalRankFusion.cs— generic RRF primitive (shared with hybrid RAG retrieval)Domain.AI/KnowledgeGraph/Models/GraphNodeMemoryExtensions.cs— abstraction + cue anchors stored in node propertiesDomain.Common/Config/AI/HarmonicMemory/HarmonicMemoryConfig.cs— the mode toggle + cost/recall knobs
Memory decay tiers
Three tiers with configurable exponential half-lives: CRITICAL (never decay),
STANDARD (slow decay), EPHEMERAL (fast decay). Scheduled
pruning removes anything below threshold.
Infrastructure.AI.RAG/GraphRag/MemoryDecayService.cs
The decay service is inert on its own — it only ages weights when something calls it. A
BackgroundService, MemoryDecayScheduler
(Infrastructure.AI.RAG/GraphRag/MemoryDecayScheduler.cs), drives it on a timer:
each pass runs ApplyDecayAsync then PruneAsync. It is
opt-in, default OFF — registered only when
AppConfig:AI:Rag:CrossSessionMemory:DecayScheduler:Enabled is true, so
cloning the template never silently starts mutating stored memory weights. Cadence comes from
...:DecayScheduler:Interval (default 6 hours) and the prune cut-off from
...:CrossSessionMemory:PruneThreshold.
Known limitation. The scheduler's Interval is read once at
service start; changing it at runtime needs a host restart to take effect. The prune threshold,
by contrast, is re-read live on every pass (via IOptionsMonitor), so it hot-reloads
without a restart.
Entity-level provenance
DefaultProvenanceStamper stamps every node and every edge with source pipeline,
task ID, and timestamp at the moment of write. Enables audit trails, debugging, and compliance.
Provenance/DefaultProvenanceStamper.cs
Compliance-aware store & right-to-erasure
ComplianceAwareGraphStore wraps any backend with retention enforcement
(ConfigRetentionPolicyProvider) and an erasure surface
(DefaultErasureOrchestrator). Erasure produces a signed ErasureReceipt
you can show to a regulator. RetentionEnforcementService runs as a background
sweep.
Multi-tenant knowledge isolation
TenantIsolatedGraphStore wraps every read/write with a call to
KnowledgeScopeValidator; the scope (user → dataset → owner) flows from
KnowledgeScopeAccessor. Multiple agents on shared infrastructure cannot see each
other's data — and the validator is a single chokepoint to audit.
Scoping/TenantIsolatedGraphStore.csScoping/KnowledgeScopeValidator.csScoping/KnowledgeScopeAccessor.cs
Graph audit sinks
Every graph mutation flows to an audit sink — StructuredLoggingAuditSink for
production, NoOpAuditSink for dev. The interface lets you bolt on Sentinel,
Splunk, or a custom append-only ledger without touching the store.
Skill effectiveness tracking on the graph
GraphSkillEffectivenessTracker records which skills helped which turns;
GraphSkillAmendmentProvider uses those records to propose skill amendments via
the learnings system. Skills get better over time without manual edits.
5 · Governance & resilience patterns
The "things that prevent the agent from quietly going wrong" layer. Drift detection, learnings, escalation, autonomy tiers, circuit breakers, fallbacks.
Drift detection — EWMA
Exponentially Weighted Moving Average baselines for quality signals (faithfulness scores, tool
success rates, escalation rates). EwmaDriftScorer computes the current value;
DriftSeverityClassifier labels it; DriftEscalationBridge escalates
on threshold breach.
Infrastructure.AI/DriftDetection/DefaultDriftDetectionService.cs.../EwmaDriftScorer.cs.../DriftSeverityClassifier.cs.../GraphEwmaStateStore.cs·InMemoryDriftBaselineStore.cs·GraphDriftBaselineStore.cs.../CompositeDriftNotifier.cs·DriftEscalationBridge.cs
Learnings store with decay
CQRS-based knowledge capture: lessons learned from a turn are persisted with an exponential-decay
score; LearningsPruningBackgroundService removes expired entries;
LearningsDriftBridge triggers re-learning when drift is detected.
Infrastructure.AI/Learnings/DefaultLearningDecayService.cs.../LearningsPruningBackgroundService.cs.../LearningsDriftBridge.cs- Store:
Infrastructure.AI.KnowledgeGraph/Learnings/GraphLearningsStore.cs·InMemoryLearningsStore.cs
Escalation workflows
Multi-approver workflows with three composition modes — AllOf (every approver must say yes), AnyOf (one is enough), Quorum (majority). Audit trail is JSONL; notifiers ship for Slack and Teams (no-op variants for dev).
Infrastructure.AI/Escalation/DefaultEscalationService.cs.../CompositeEscalationNotifier.cs.../NoOpSlackNotifier.cs·NoOpTeamsNotifier.cs
Autonomy tiers
Three levels enforced by the IToolInvocationGovernor on the tool path:
Manual (human approves every action), Supervised
(auto-approve low-risk, escalate high-risk), Autonomous (no gates, full audit).
Tier resolves per-plugin, per-skill, or per-tool via DefaultAutonomyTierResolver.
Resilience pipeline (Polly)
ProviderResiliencePipelineBuilder assembles retry + circuit breaker + timeout +
fallback into a single Polly pipeline per provider. Default circuit breaker: 30s open after
repeated failures, 120s half-open probe; default retry: exponential 1s → 60s.
ResilientChatClient applies the pipeline transparently.
Infrastructure.AI/Resilience/ProviderResiliencePipelineBuilder.cs.../ResilientChatClient.cs.../ProviderCapabilityRegistry.cs.../LlmRetryQueue.cs
Provider fallback chains
If Azure OpenAI fails the circuit breaker, the pipeline falls through to Anthropic, then to a local echo client (in dev) or escalation (in prod). The dashboard tracks "intended provider" versus "effective provider" so the operator can see how often fallbacks are firing.
Response sanitisers
ResponseSanitizationBehavior masks PII / secrets in outbound responses;
PatternSecretRedactor applies a configurable regex set (JWT, API keys, connection
strings) before logs or wire payloads ever touch storage.
Hooks system
Plugin-declared pre/post hooks fire around tool calls and pipeline phases.
InMemoryHookRegistry holds the declarations; CompositeHookExecutor
invokes them in declared order with cancellation support. Used by safety gates, observability
spans, and policy enforcement.
Safety gates & denial tracking
SafetyGateRegistry holds named gates a plugin can flip
(before_write, before_shell, etc.); InMemoryDenialTracker
records every denial so operators can see what the harness is blocking and why.
Tool-invocation governor (three ambient gates)
The live tool-gating path. Every agent tool is wrapped by GovernedAIFunction, whose
InvokeCoreAsync runs three ambient gates before the tool executes:
IToolInvocationGovernor (authorization — permission resolver → graded-autonomy risk
gate → capability enforcement → YAML policy engine; fails closed with a
PendingApproval), IToolClassificationGate (data-classification DLP,
below), and IProgressEvaluator (spin / no-progress guard). Opt-in via
GovernanceConfig.EnforceToolInvocation. Files under
Application.AI.Common/Services/Governance/ and
Services/Tools/GovernedAIFunction.cs.
Purview data-classification DLP
IToolClassificationGate classifies tool arguments/outputs against Microsoft Purview
sensitivity labels. Opt-in via AppConfig:AI:Governance:DataClassification:Mode
(Off / Audit / Enforce). A Block verdict
returns a model-facing message instead of running the tool; a RedactOutput
verdict runs the tool then scrubs the result via ICompositeResponseSanitizer.
Defaults Off and Unknown-classification → Allow.
Trust-aware memory write-gate
KnowledgeMemoryService.RememberAsync runs IMemoryWriteGate on every
memory write. Untrusted content is quarantined (MemoryTrust.Untrusted) rather than
written as recallable; IsRecallable is re-checked at read time so quarantined
entries never re-enter a prompt. Defends the "poisoned memory" path (OWASP ASI06).
Conversation budget & spin guard
IConversationBudgetTracker is a singleton that enforces a conversation-lifetime
token ceiling (distinct from the per-turn IContextBudgetTracker), breaking
gracefully when exceeded. The spin guard IProgressEvaluator halts a tool call when
the agent repeats identical calls without progress.
Tamper-evident audit hash-chain
HashChainedJsonlWriter links every audit record to its predecessor by hash;
AuditChainVerificationService / IVerifiableAuditChain detect any
insertion, deletion, or edit of past records.
OWASP Agentic Top-10 evals
Ten metrics OwaspAsi01…10* under
Application.AI.Common/Evaluation/Metrics/Owasp/, keyed-registered and run via
dotnet test --filter Category=OwaspAgentic against the dataset at repo-root
eval-datasets/owasp-agentic-top-10.yaml. See
OWASP Evals.
6 · Plan execution patterns
Long-running multi-step work — research, planning, codegen pipelines — runs as a
Plan: a DAG of PlanStep nodes with checkpoint/resume, error recovery,
and per-step observability. Lives in Infrastructure.AI/Planner/.
DAG executor (marquee pattern · 11 of 12)
PlanExecutor walks the DAG in topological order with bounded concurrency, checkpointing
to EF Core between batches. Each step resolves its keyed IPlanStepExecutor based
on StepType. Recovery, scheduling, and summary live in partial classes.
public interface IPlanStepExecutor
{
StepType Type { get; } // keyed-DI registration key
Task<StepResult> ExecuteAsync(PlanStep step, PlanExecutionContext ctx, CancellationToken ct);
}
- Core:
Planner/PlanExecutor.cs - Scheduling:
Planner/PlanExecutor.Scheduling.cs - Recovery:
Planner/PlanExecutor.Recovery.cs - Summary:
Planner/PlanExecutor.Summary.cs - Plan generation:
Planner/LlmPlanGeneratorService.cs - Plan validation:
Planner/PlanValidator.cs - Plan output mapping:
Planner/LlmPlanOutputMapper.cs
Checkpoint & resume
EfCorePlanStateStore persists plan + step state to SQLite (or any EF Core
provider). The store is split across .cs (writes), .Reads.cs, and
.Mappers.cs partials. Versions are auto-incremented by the
SqliteVersionInterceptor on save.
Error recovery
Three per-step policies declared in the plan: Retry (bounded exponential
backoff), Escalate (raise a HumanGate), Skip (mark N/A,
continue downstream). Logic lives in PlanExecutor.Recovery.cs.
Step executors (keyed by StepType)
LlmCallStepExecutor— invokes an LLM through the factory, streams tokens (StepExecutors/LlmCallStepExecutor.cs)ToolUseStepExecutor— runs a single tool through the sandbox (StepExecutors/ToolUseStepExecutor.cs)HumanGateStepExecutor— pauses execution, awaits human approval (StepExecutors/HumanGateStepExecutor.cs)ConditionalBranchStepExecutor— evaluates condition, routes to next (StepExecutors/ConditionalBranchStepExecutor.cs)SubPlanStepExecutor— nests another plan inside this step (StepExecutors/SubPlanStepExecutor.cs)RetrievalPlanStepExecutor— invokes the RAG pipeline as a planned step (StepExecutors/RetrievalPlanStepExecutor.cs)
Step summary & observation
Every step emits an ObservationOutput (structured result + metadata). The
PlanExecutor.Summary.cs partial aggregates them into observation trees that feed
the knowledge graph and the trace exporter.
7 · Sandbox & isolation patterns
Tool execution is closed-by-default: a tool runs only inside a sandbox executor, the executor enforces resource limits, and every call is signed for audit.
Process sandbox (Windows Job Objects)
ProcessSandboxExecutor spawns tools as subprocesses with JSON-over-stdin/stdout
I/O. On Windows it wraps each child in a Job Object via WindowsJobObjectManager
+ WindowsProcessResourceLimiter to enforce CPU, memory, and handle limits. Other
platforms fall through to NoOpProcessResourceLimiter (execution still works,
limits log a warning).
Infrastructure.AI/Sandbox/ProcessSandboxExecutor.cs.../WindowsJobObjectManager.cs.../WindowsProcessResourceLimiter.cs.../NoOpProcessResourceLimiter.cs
Docker sandbox
DockerSandboxExecutor pulls a per-tool container image, spins up a container
with strict CPU/memory limits, communicates via JSON, and tears down. Strongest isolation;
cost is image-pull latency.
Infrastructure.AI/Sandbox/DockerSandboxExecutor.cs
HMAC attestation (marquee pattern · 12 of 12)
Every sandboxed execution is signed by HmacAttestationService: input hash, output
hash (on success) or reason (on failure), and a timestamp, signed with a per-deployment HMAC
key. Attestations persist via EfCoreAttestationStore and can be verified
out-of-band — useful when a regulator asks "did this agent really call that tool with that
input?".
var inputHash = ComputeSha256(canonicalJson(input));
var outputHash = result.IsSuccess ? ComputeSha256(canonicalJson(result.Value)) : null;
var attestation = new AttestationRecord(
ToolName: step.ToolName,
InputHash: inputHash,
OutputHash: outputHash,
Outcome: result.IsSuccess ? Outcome.Success : Outcome.Failure,
Reason: result.IsSuccess ? null : result.Error,
Timestamp: _clock.UtcNow);
attestation = attestation with { Signature = _hmac.Sign(attestation, _keyOptions.CurrentKey) };
await _store.SaveAsync(attestation, ct);
Infrastructure.AI/Attestation/HmacAttestationService.cs.../EfCoreAttestationStore.cs.../AttestationKeyOptions.cs+AttestationKeyOptionsValidator.cs
Tool permission gate
IToolInvocationGovernor (called from GovernedAIFunction on the tool
path) is the first line of defence — its AuthorizeAsync invokes
ThreePhasePermissionResolver, which checks the invoking plugin's
AllowedTools/DeniedTools against the requested tool and applies
"default deny" → "plugin allow" → "user deny" precedence; GlobPatternMatcher
supports wildcard rules.
Secret redaction
PatternSecretRedactor scans every outbound payload (logs, responses, tool inputs)
against a configurable pattern set (JWT, AWS keys, GitHub PATs, Azure connection strings) and
replaces matches before the data leaves the process.
8 · Cross-cutting infrastructure
FluentValidation auto-discovery
Every DTO has an AbstractValidator<T>; the DI bootstrap scans assemblies and
registers them all. Validation runs as the outermost MediatR behaviour; failures short-circuit
with a Result<T>.ValidationFailure(errors).
AutoMapper profiles
Entity ↔ DTO mapping. Profiles auto-registered via DI scanning. Used heavily in the API layer and in the EF Core state stores (entity ↔ plan model).
EF Core + IDbContextFactory
Every consumer takes IDbContextFactory<TDbContext> rather than the context
directly — short-lived contexts per unit of work, no captive dependency issues.
SqliteVersionInterceptor auto-increments entity Version on save.
Structured logging
All logs flow through ILogger<T> as JSONL. Providers:
NamedPipeLoggerProvider streams to LoggerUI in dev;
FileLoggerProvider + StructuredJsonLoggerProvider persist to ndjson
files; Application Insights in production.
OpenTelemetry instrumentation
Spans propagate via W3C TraceContext. The GenAI semantic conventions
(gen_ai.system, gen_ai.request.model,
gen_ai.usage.input_tokens, etc.) are mapped through a single registry to avoid
drift between subsystems.
Application.AI.Common/OpenTelemetry/Conventions/GenAiSemconvRegistry.cs(single source of truth).../AiTelemetryConfigurator.cs- Blueprint:
documentation/blueprints/agentic-harness-observability.md
Metrics inventory
BudgetMetrics— token spend, cost trackingLlmUsageMetrics— prompt / completion / cached tokens, per providerToolExecutionMetrics— count, latency, error rate per toolContentSafetyMetrics— violation counts by category & severityDriftMetrics— current vs. baseline quality scoresEscalationMetrics— approvals, timeouts, rejectionsResilienceMetrics— circuit-breaker state, retries, fallbacks
All under src/Content/Application/Application.AI.Common/OpenTelemetry/Metrics/.
Observability exporters (multi-backend)
- Grafana Tempo — traces in local dev (docker-compose)
- Prometheus — metrics scrape endpoint
- Azure Monitor — production OTLP destination
- SignalRSpanExporter — custom exporter that streams spans to the dashboard hub without back-pressure
Lives in src/Content/Infrastructure/Infrastructure.Observability/.
SignalR AgentHub (real-time telemetry)
Long-running agent turns stream their span data, log lines, and partial responses through a SignalR hub to the React dashboard. Resilience config: 120-second timeouts, 30-second keepalive, infinite exponential-backoff reconnect — tuned for LLM workloads where a single turn can take minutes.
src/Content/Presentation/Presentation.AgentHub/Telemetry/SignalRSpanExporter.cs
Authentication & authorisation
DevAuthHandler mints a test JWT with roles in development — gated by a
double-check (config flag + presence of an Azure SPA client ID) so it can't accidentally ship
to prod. Production uses Azure AD / Entra ID with full JWT validation
(ValidateLifetime=true, ClockSkew=Zero). Rate limiting per identity.
MCP server
ASP.NET Core endpoints under /mcp/... expose the harness's tools, prompts, and
resources to other agents over the Model Context Protocol. JWT-authenticated; tools surface
through the same keyed-DI registrations the harness uses internally.
Budget tracking
Cumulative token spend per conversation, observable via BudgetMetrics +
TokenBudgetBehavior. Threshold breaches trigger escalation; the dashboard renders
live spend.
9 · Frontend patterns (Presentation.Dashboard)
The dashboard is a Vite-built React SPA living at
src/Content/Presentation/Presentation.Dashboard/. It renders live agent telemetry,
budget/cost panes, evaluation runs, the catalog inspector, and the context-window inspector.
Stack
- React 19 + TypeScript ~6.0
- Vite 8 — dev server on
:5174, proxies to AgentHub;npm run dev:alllaunches both - Tailwind CSS 4 (with
@tailwindcss/vite) +tw-animate-css - Radix UI primitives (
react-dialog,react-select) + shadcn/ui patterns - Zustand — global client state
- TanStack Query — server-state cache, fetching, mutations
- Recharts — telemetry & budget charts
- @microsoft/signalr — real-time stream from AgentHub
- @azure/msal-browser + @azure/msal-react — Entra ID auth
- axios + date-fns + lucide-react icons + class-variance-authority / clsx / tailwind-merge for component variants
Auth gating
Dev mode is unlocked when VITE_AZURE_SPA_CLIENT_ID is absent — no separate
IS_AUTH_DISABLED env var. Presence of the client ID flips the SPA into MSAL mode.
SignalR contract discipline
Client .on() handlers must match server SendAsync(event, new { ... })
property names exactly. This is a recurring bug class — when changing telemetry events,
update both sides in the same diff.
Frontend testing
- Vitest 4 — unit & integration tests
- @testing-library/react + jest-dom + user-event
- msw — HTTP mocking
- jsdom — DOM environment
- @vitest/coverage-v8 — coverage
- Playwright 1.59 — E2E (
test:e2e,test:e2e:headed,test:e2e:debug)
10 · Tech stack inventory
.NET / NuGet packages
Agent & LLM
Microsoft.Agents.AI— Microsoft Agent Framework coreMicrosoft.Extensions.AI— provider-agnostic AI abstractionsAzure.AI.Agents.Persistent— AI Foundry persistent agentsAzure.AI.OpenAI— Azure OpenAI clientAnthropic.SDK— Anthropic direct API
RAG & knowledge
ManagedCode.GraphRag— GraphRAG building blocksAzure.Search.Documents— Azure AI Search (dense + BM25 + semantic ranker)- FAISS bindings — local vector store
Neo4j.Driver— Neo4j graph database- Kuzu — embedded analytical graph DB
Npgsql— PostgreSQL (with optional graph extensions)- SQLite FTS5 — local BM25 index
Resilience
Polly+Polly.Extensions.Http— circuit breakers, retry, fallback, timeout
Observability
OpenTelemetry.Api,OpenTelemetry.Extensions.HostingOpenTelemetry.Exporter.Prometheus.AspNetCoreOpenTelemetry.Exporter.OpenTelemetryProtocol(OTLP — Tempo, Azure Monitor)Azure.Monitor.OpenTelemetry.AspNetCoreOpenTelemetry.Instrumentation.AspNetCore+.Http+.EntityFrameworkCore
Persistence
Microsoft.EntityFrameworkCore+.Sqlite+.InMemoryMicrosoft.EntityFrameworkCore.Tools+.Design(migrations)
Sandboxing & tooling
Docker.DotNet— Docker client for the container sandbox- Win32 P/Invoke for Job Objects (Windows-only resource limits)
API & messaging
MediatR— CQRS bus + pipeline behavioursFluentValidation+FluentValidation.AspNetCoreAutoMapper+AutoMapper.Extensions.Microsoft.DependencyInjectionMicrosoft.AspNetCore.SignalRMicrosoft.AspNetCore.Authentication.JwtBearer+ Microsoft Identity Web for Entra ID
Console & UI
Spectre.Console— TUI rendering in ConsoleUI
Testing
xunit+xunit.runner.visualstudioMicrosoft.NET.Test.SdkMoq+Microsoft.Extensions.TimeProvider.TestingFluentAssertionsMicrosoft.AspNetCore.Mvc.Testing— WebApplicationFactorycoverlet.collector— coverageXunit.SkippableFact— conditional skips (e.g. credentialled Azure tests)
Frontend npm dependencies
Covered in detail under §9 Frontend stack. Highlights: React 19, Vite 8, Tailwind 4, Zustand, TanStack Query, Radix/shadcn, Recharts, MSAL, @microsoft/signalr. Test stack: Vitest 4, Testing Library, msw, Playwright.
Infrastructure / Azure
- Compute — Azure Container Apps (validated deployment topology), with Neo4j-on-AKS
- AI — Azure OpenAI, Azure AI Foundry (persistent agents), Azure AI Inference, OpenRouter as alternate provider
- Retrieval — Azure AI Search (dense + sparse + semantic reranker), Bing Web Search
- Graph — Neo4j (AKS), PostgreSQL (managed), Kuzu (in-pod)
- State — SQLite (local), PostgreSQL (managed) for plan state & observability
- Observability — Application Insights, Azure Monitor; Grafana + Tempo + Prometheus for local dev
- Identity — Microsoft Entra ID (Azure AD)
- Secrets — User Secrets (dev), Azure Key Vault (prod)
- IaC — Bicep (planned for the validated topology)
- Docs deployment — GitHub Actions (
.github/workflows/pages.ymlpublishes the four docs sites — onboarding, architecture, course, reference — on push to main) - Delivery rails — agentic CI/CD governance:
ci.yml(build/test + the blocking OWASP Agentic Top-10 gate),security-review.yml(path-gated security reviewer, blocks on HIGH), andgrader.yml(advisory spec grader). See the Developer Guide's Delivery & CI Governance chapter.
11 · Testing patterns
xUnit conventions
Test classes named *Tests; methods named
MethodName_Scenario_ExpectedResult. [Fact] for single cases,
[Theory] with [InlineData] for parameterised. One test project per
source project under src/Content/Tests/<project>.Tests/.
WebApplicationFactory integration tests
TestWebApplicationFactory<Program> spins up the full host in-process. A
TestAuthHandler mints test-user JWTs; an in-memory EF Core provider replaces
SQLite; MediatR mocks provide deterministic agent responses (no real LLM calls).
Moq usage
Reserved for external services — IChatClient, HTTP handlers, TimeProvider,
third-party APIs. Never used for value objects, DTOs, or the thing under test.
Time mocking
Microsoft.Extensions.TimeProvider.Testing's FakeTimeProvider for any
code involving timestamps, decay, or scheduled work — drift baselines, memory decay, learnings
pruning all need this.
Coverage
coverlet.collector generates Cobertura XML during dotnet test --collect:"XPlat Code Coverage";
target is 80% on new code. CI gates on the threshold.
Frontend testing
Covered in §9 Frontend testing: Vitest + Testing Library + msw
for unit/integration, Playwright for E2E. Tests live alongside source under
Presentation.Dashboard/src/; E2E under Presentation.Dashboard/tests/e2e/.
Where to go from here
The six-pattern primer in the onboarding guide. Read first if any of the above felt dense.
↩ NarrativeWatch most of the above patterns fire in sequence for a single user prompt.
→ OperationsWhere these patterns run in Azure — topology, networking, costs, scaling.