Reference · 01

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.

i
Looking for the gentle intro?

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 rules
  • src/Content/Application/ — interfaces, CQRS, MediatR behaviours
  • src/Content/Infrastructure/ — EF Core, AI clients, graph backends, observability
  • src/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.

C# · IPipelineBehavior shape
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):

  1. UnhandledExceptionBehavior — top-level safety net, structured log + rethrow
  2. AmbientRequestScopeBehavior — establishes the ambient request scope
  3. AgentContextPropagationBehavior — copies agent/conversation IDs into ambient OTel baggage
  4. AgentIdentityResolutionBehavior — resolves the acting agent identity
  5. AuditTrailBehavior — JSONL audit log of every command
  6. ContentSafetyBehavior — runs Azure Content Safety (or mock) on inputs & outputs
  7. PromptInjectionBehavior — guards against user-prompted system-prompt overrides
  8. TokenBudgetBehavior — enforces per-conversation token spend
  9. HookBehavior — fires pre/post hooks declared by plugins
  10. RetrievalAuditBehavior — captures RAG retrievals for evaluation replay
  11. ResponseSanitizationBehavior — masks PII/secrets before returning to client (post-execution)
  12. ToolOutputCompressionBehavior — compresses large tool outputs by content type (post-execution)
  13. KnowledgeExtractionBehavior — extracts facts from conversation into the knowledge graph (post-turn, fire-and-forget)
  14. WorkEpisodeCaptureBehavior — captures work episodes for self-improving memory (post-turn, fire-and-forget)
  15. 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 (IToolInvocationGovernorIToolClassificationGateIProgressEvaluator) 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.

C# · src/Content/Domain/Domain.Common/Result.cs
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:

AgentFactory
Builds a fully-configured AIAgent — loads skills, resolves tools, wires content safety and OTel, applies function-invocation limits. src/Content/Application/Application.AI.Common/Factories/AgentFactory.cs
ChatClientFactory
Builds an LLM IChatClient 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.cs
AgentExecutionContextFactory
Builds the per-turn execution context — conversation IDs, OTel session, ambient services. src/Content/Application/Application.AI.Common/Factories/AgentExecutionContextFactory.cs
C# · sketch of the ChatClientFactory shape
public 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
}
The rule

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.

C# · keyed registration & resolution
// 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
  • RerankersAzureSemantic, CrossEncoder, NoOp
  • Chunking strategiesStructureAware, FixedSize, Semantic
  • Graph backendsNeo4j, Kuzu, PostgreSql, InMemory
  • Sandbox executorsProcess, Docker
  • Plan step executors — keyed by StepType enum
  • Compaction strategiesFull, 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).

C# · src/Content/Infrastructure/Infrastructure.AI/Resilience/ResilientChatClient.cs
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:

  • ComplianceAwareGraphStore wraps any IGraphStore with retention enforcement & erasure orchestration
  • TenantIsolatedGraphStore wraps a graph store with scope-validation calls
  • MarkdownCheckpointDecorator wraps the JSON checkpoint state manager with a Markdown side-write for humans
  • CompositeStateManager wraps 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.

C# · plan execution sketch
// 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 per StepType
  • 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.

C# · idiomatic config access
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.cs
  • EfCorePlanStateStore.cs + .Reads.cs + .Mappers.cs
  • KuzuGraphBackend.cs + .Crud.cs + .Community.cs
  • LeidenCommunityDetector.cs + .Algorithm.cs + .GraphConstruction.cs
  • ChatClientFactory.cs + .Providers.cs
  • RagOrchestrator.cs + .MultiHop.cs
  • DependencyInjection.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:

Managed mode
The harness resolves each declared tool — preferring an MCP server that exposes it, falling back to keyed DI. Used by skills that should work even if their plugin's MCP server is offline.
Injected mode
The skill receives all tools from its owning plugin's MCP servers, plus the keyed-DI fallbacks. Used by plugins that ship a curated tool surface as a coherent set.
Markdown · src/.../Agents/Skills/<skill>/SKILL.md
---
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 JsonSchema via Microsoft.Extensions.AI's AITool conversion

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.

C# · sketch of HybridRetriever
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.

C# · sketch of the CRAG decision
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.cs partial)
  • Orchestration/MultiSourceOrchestrator.cs
  • Orchestration/VectorRetrievalSource.cs
  • Orchestration/GraphRetrievalSource.cs
  • SqlDatabase/SqlDatabaseRetrievalSource.cs
  • WebSearch/WebSearchRetrievalSource.cs (Bing-backed by default)
  • Orchestration/RetrievalDecisionGate.cs
  • Orchestration/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.cs partials (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

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.cs
  • Feedback/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.cs
  • Memory/InMemorySessionCache.cs
  • Memory/ConversationFactExtractor.cs
  • Infrastructure.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 fusion
  • Domain.AI/Retrieval/ReciprocalRankFusion.cs — generic RRF primitive (shared with hybrid RAG retrieval)
  • Domain.AI/KnowledgeGraph/Models/GraphNodeMemoryExtensions.cs — abstraction + cue anchors stored in node properties
  • Domain.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.cs
  • Scoping/KnowledgeScopeValidator.cs
  • Scoping/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.

C# · step executor interface
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?".

C# · attestation flow sketch
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 tracking
  • LlmUsageMetrics — prompt / completion / cached tokens, per provider
  • ToolExecutionMetrics — count, latency, error rate per tool
  • ContentSafetyMetrics — violation counts by category & severity
  • DriftMetrics — current vs. baseline quality scores
  • EscalationMetrics — approvals, timeouts, rejections
  • ResilienceMetrics — 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:all launches 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 core
  • Microsoft.Extensions.AI — provider-agnostic AI abstractions
  • Azure.AI.Agents.Persistent — AI Foundry persistent agents
  • Azure.AI.OpenAI — Azure OpenAI client
  • Anthropic.SDK — Anthropic direct API

RAG & knowledge

  • ManagedCode.GraphRag — GraphRAG building blocks
  • Azure.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.Hosting
  • OpenTelemetry.Exporter.Prometheus.AspNetCore
  • OpenTelemetry.Exporter.OpenTelemetryProtocol (OTLP — Tempo, Azure Monitor)
  • Azure.Monitor.OpenTelemetry.AspNetCore
  • OpenTelemetry.Instrumentation.AspNetCore + .Http + .EntityFrameworkCore

Persistence

  • Microsoft.EntityFrameworkCore + .Sqlite + .InMemory
  • Microsoft.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 behaviours
  • FluentValidation + FluentValidation.AspNetCore
  • AutoMapper + AutoMapper.Extensions.Microsoft.DependencyInjection
  • Microsoft.AspNetCore.SignalR
  • Microsoft.AspNetCore.Authentication.JwtBearer + Microsoft Identity Web for Entra ID

Console & UI

  • Spectre.Console — TUI rendering in ConsoleUI

Testing

  • xunit + xunit.runner.visualstudio
  • Microsoft.NET.Test.Sdk
  • Moq + Microsoft.Extensions.TimeProvider.Testing
  • FluentAssertions
  • Microsoft.AspNetCore.Mvc.Testing — WebApplicationFactory
  • coverlet.collector — coverage
  • Xunit.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.yml publishes 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), and grader.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