Categories
Implementation Specs (157)
- specs/AnomalyDetection/TASK1.1.md
- specs/AnomalyDetection/TASK1.2.md
- specs/AnomalyDetection/TASK1.3.md
- specs/AnomalyDetection/TASK1.4.md
- specs/AnomalyDetection/TASK2.1.md
- specs/AnomalyDetection/TASK2.2.md
- specs/AnomalyDetection/TASK3.1.md
- specs/AnomalyDetection/TASK3.2.md
- specs/AnomalyDetection/TASK3.3.md
- specs/AnomalyDetection/TASK4.1.md
- specs/AnomalyDetection/TASK4.2.md
- specs/AnomalyDetection/TASK4.3.md
- specs/AnomalyDetection/TASK4.4.md
- specs/AnomalyDetection/TASK5.1.md
- specs/AnomalyDetection/TASK5.2.md
- specs/AnomalyDetection/TASK5.3.md
- specs/AnomalyDetection/TASK6.1.md
- specs/AnomalyDetection/TASK6.2.md
- specs/AnomalyDetection/TASK6.3.md
- specs/AnomalyDetection/TASKS.md
- specs/ApiServicePostgresTestStability/TASK.md
- specs/BackendDatabaseConsolidation/TASK.md
- specs/BackendDatabaseConsolidation/TASK1.1.md
- specs/BackendDatabaseConsolidation/TASK1.2.md
- specs/BackendDatabaseConsolidation/TASK1.3.md
- specs/BackendDatabaseConsolidation/TASK2.1.md
- specs/BackendDatabaseConsolidation/TASK2.2.md
- specs/BackendDatabaseConsolidation/TASK2.3.md
- specs/BackendDatabaseConsolidation/TASK3.1.md
- specs/BackendDatabaseConsolidation/TASK3.2.md
- specs/BackendDatabaseConsolidation/TASK4.1.md
- specs/BackendDatabaseConsolidation/TASK4.2.md
- specs/BackendDatabaseConsolidation/TASK4.3.md
- specs/BackendDatabaseConsolidation/TASK5.1.md
- specs/BackendDatabaseConsolidation/TASK5.2.md
- specs/BrinkLaborGetShiftsStaging/TASK.md
- specs/BrinkPosOrdersFirstEtlJob/TASK.md
- specs/BrinkPosOrdersFirstEtlJob/TASK0.md
- specs/BrinkPosOrdersFirstEtlJob/TASK1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK5.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.1.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.2.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.3.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.4.md
- specs/BrinkPosOrdersFirstEtlJob/TASK6.md
- specs/BrinkPosOrdersFirstEtlJob/TASK7.md
- specs/BrinkPosOrdersFirstEtlJob/TASK8.md
- specs/BrinkPosOrdersFirstEtlJob/TASK9.md
- specs/BrinkSettings2ToStaging/TASK.MD
- specs/CuratedToS3Stage/TASK.md
- specs/CuratedToS3Stage/TASK0.md
- specs/CuratedToS3Stage/TASK1.md
- specs/CuratedToS3Stage/TASK2.md
- specs/CuratedToS3Stage/TASK3.md
- specs/CuratedToS3Stage/TASK4.md
- specs/CustomerCredentialCustomHeaders/TASK.md
- specs/CustomerCredentialCustomHeaders/TASK1.1.md
- specs/CustomerCredentialCustomHeaders/TASK1.2.md
- specs/CustomerCredentialCustomHeaders/TASK2.1.md
- specs/CustomerCredentialCustomHeaders/TASK2.2.md
- specs/CustomerCredentialCustomHeaders/TASK3.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK.md
- specs/CustomerCredentialTriggeredProvisioning/TASK1.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK1.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK2.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK2.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK3.1.md
- specs/CustomerCredentialTriggeredProvisioning/TASK3.2.md
- specs/CustomerCredentialTriggeredProvisioning/TASK4.1.md
- specs/EtlSourceAdapterFramework/TASK.md
- specs/EtlSourceAdapterFramework/TASK0.md
- specs/EtlSourceAdapterFramework/TASK1.md
- specs/EtlSourceAdapterFramework/TASK2.md
- specs/EtlSourceAdapterFramework/TASK3.md
- specs/EtlSourceAdapterFramework/TASK4.md
- specs/EtlSourceAdapterFramework/TASK5.md
- specs/EtlSourceAdapterFramework/TASK6.md
- specs/KdsCdcDebeziumIntegration/TASK.md
- specs/KdsCdcDebeziumIntegration/TASK1.md
- specs/KdsCdcDebeziumIntegration/TASK2.md
- specs/KdsCdcDebeziumIntegration/TASK3.md
- specs/KdsCdcDebeziumIntegration/TASK4.md
- specs/KdsCdcDebeziumIntegration/TASK5.md
- specs/KdsCdcDurableRelationRouting/TASK.md
- specs/KdsCdcDurableRelationRouting/TASK1.md
- specs/KdsCdcDurableRelationRouting/TASK2.md
- specs/KdsCdcDurableRelationRouting/TASK3.md
- specs/KdsCdcDurableRelationRouting/TASK4.md
- specs/KdsCdcDurableRelationRouting/TASK5.md
- specs/KdsCdcIngestion/TASK.md
- specs/KdsCdcIngestion/TASK1.md
- specs/KdsCdcIngestion/TASK2.md
- specs/KdsCdcIngestion/TASK3.md
- specs/KdsCdcIngestion/TASK4.md
- specs/KdsCdcIngestion/TASK5.md
- specs/KdsCdcIngestion/TASK6.md
- specs/KdsCdcIngestion/TASK7.md
- specs/KdsCdcIngestion/TASK8.md
- specs/KdsCdcIngestion/TASK9.md
- specs/KdsCdcReplayExecution/TASK.md
- specs/KdsCdcReplayExecution/TASK1.md
- specs/KdsCdcReplayExecution/TASK2.md
- specs/KdsCdcReplayExecution/TASK3.md
- specs/KdsCdcReplayExecution/TASK4.md
- specs/KdsMonitorServingContractMigration/TASK.md
- specs/KdsMonitorServingContractMigration/TASK1.1.md
- specs/KdsMonitorServingContractMigration/TASK1.2.md
- specs/KdsMonitorServingContractMigration/TASK2.1.md
- specs/KdsMonitorServingContractMigration/TASK2.2.md
- specs/KdsMonitorServingContractMigration/TASK3.1.md
- specs/KdsMonitorServingContractMigration/TASK3.2.md
- specs/RedshiftFoundation/TASK.md
- specs/RedshiftFoundation/TASK1.1.md
- specs/RedshiftFoundation/TASK1.2.md
- specs/RedshiftFoundation/TASK1.3.md
- specs/RedshiftFoundation/TASK1.4.md
- specs/RedshiftFoundation/TASK2.1.md
- specs/RedshiftFoundation/TASK2.2.md
- specs/RedshiftFoundation/TASK3.1.md
- specs/RedshiftFoundation/TASK3.2.md
- specs/RedshiftFoundation/TASK3.3.md
- specs/RedshiftSyntheticDataFeed/TASK.md
- specs/RedshiftSyntheticDataFeed/TASK1.1.md
- specs/RedshiftSyntheticDataFeed/TASK1.2.md
- specs/RedshiftSyntheticDataFeed/TASK1.3.md
- specs/RedshiftSyntheticDataFeed/TASK2.1.md
- specs/RedshiftSyntheticDataFeed/TASK2.2.md
- specs/RedshiftSyntheticDataFeed/TASK3.1.md
- specs/RedshiftSyntheticDataFeed/TASK3.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK1.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK1.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK2.3.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK3.1.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK3.2.md
- specs/SingleStoreRedshiftRuntimeReadPath/TASK4.1.md
TASK 2.2: Implement AnomalyDetectorAgent with LLM + MCP Integration
Summary
Implement the anomaly detection agent that uses LLM reasoning with MCP tools to detect anomalies. The agent is fully generic - it receives a MonitorSpec (plain text instructions) and MCP tools (database access), and relies on the UseFunctionInvocation() middleware to handle the agentic tool-calling loop automatically.
Dependencies
- TASK 1.4 (McpToolProvider for MCP tools)
- TASK 2.1 (IAnomalyDetectorAgent interface, result types)
Architecture
MonitorSpec.SystemPrompt ──► LLM (Claude via Bedrock)
│
UseFunctionInvocation() middleware
handles tool loop automatically
│
├── calls mysql_query (MCP tool) ──► MySQL DB
├── calls mysql_query again...
├── reasons about results...
└── calls emit_conclusion ──► AnomalyDetectionResultHow UseFunctionInvocation() Works
The FunctionInvokingChatClient middleware (added via UseFunctionInvocation() on the IChatClient pipeline) intercepts GetResponseAsync calls and:
- Sends the request to the inner chat client (LLM)
- If the LLM response contains tool call requests (
FunctionCallContent), the middleware automatically invokes the correspondingAIToolfunctions - Appends the tool results to the conversation and sends it back to the LLM
- Repeats until the LLM produces a response with no tool calls (or max iterations reached)
- Returns the final
ChatResponsewith the full conversation history
This means the agent code does NOT need a manual for loop - a single GetResponseAsync call handles the entire multi-turn tool-calling conversation.
Agent Flow
- System prompt from MonitorSpec + user message with analysis time
- Single
GetResponseAsynccall with all tools (MCP + emit_conclusion) - Middleware handles the entire loop: LLM → tool calls → tool results → LLM → ...
- When LLM calls
emit_conclusion, the tool captures the structured result - When LLM stops calling tools,
GetResponseAsyncreturns - Agent checks if
emit_conclusionwas called and returns the result
Files to Create
1. backend/Kds.Ai.AnomalyDetector/Agents/ConclusionHolder.cs
A simple mutable wrapper to capture the emit_conclusion tool result. This avoids the ref closure bug (closures cannot capture ref parameters).
namespace Kds.Ai.AnomalyDetector.Agents;
/// <summary>
/// Mutable holder for the agent's conclusion, captured by the emit_conclusion tool closure.
/// </summary>
internal sealed class ConclusionHolder
{
public AnomalyDetectionResult? Result { get; set; }
}2. backend/Kds.Ai.AnomalyDetector/Agents/AnomalyDetectorAgent.cs
using System.Text.Json;
using Kds.Ai.AnomalyDetector.Mcp;
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
namespace Kds.Ai.AnomalyDetector.Agents;
public sealed class AnomalyDetectorAgent : IAnomalyDetectorAgent
{
private readonly IChatClient _chatClient;
private readonly McpToolProvider _mcpToolProvider;
private readonly ILogger<AnomalyDetectorAgent> _logger;
public AnomalyDetectorAgent(
IChatClient chatClient,
McpToolProvider mcpToolProvider,
ILogger<AnomalyDetectorAgent> logger)
{
_chatClient = chatClient;
_mcpToolProvider = mcpToolProvider;
_logger = logger;
}
public async Task<AnomalyDetectionResult> RunDetectionAsync(
MonitorSpec spec,
DateTimeOffset analysisTime,
CancellationToken ct = default)
{
_logger.LogInformation(
"Starting anomaly detection for monitor '{MonitorName}' at {AnalysisTime}",
spec.Name, analysisTime);
try
{
// 1. Get MCP tools (mysql_query etc.)
var mcpTools = await _mcpToolProvider.GetToolsAsync(ct);
// 2. Create the emit_conclusion tool for structured output
var holder = new ConclusionHolder();
var emitConclusionTool = CreateEmitConclusionTool(holder);
// 3. Combine all tools
var allTools = mcpTools.Concat([emitConclusionTool]).ToList();
// 4. Build conversation
var messages = new List<ChatMessage>
{
new(ChatRole.System, spec.SystemPrompt),
new(ChatRole.User, $"Run analysis. Current time: {analysisTime:yyyy-MM-dd HH:mm:ss} UTC")
};
// 5. Single call - UseFunctionInvocation() middleware handles the agentic loop
var response = await _chatClient.GetResponseAsync(
messages, new ChatOptions { Tools = allTools }, ct);
// 6. Extract token usage from the response
var totalTokens = 0;
if (response.Usage is not null)
totalTokens = (response.Usage.InputTokenCount ?? 0) +
(response.Usage.OutputTokenCount ?? 0);
// 7. Build reasoning trace from conversation history
var reasoning = BuildReasoningTrace(response);
// 8. Check if emit_conclusion was called
if (holder.Result is not null)
{
return holder.Result with
{
Reasoning = reasoning,
TotalTokens = totalTokens
};
}
// Agent finished without calling emit_conclusion
_logger.LogWarning(
"Agent for '{MonitorName}' finished without calling emit_conclusion",
spec.Name);
return new AnomalyDetectionResult
{
AnomalyDetected = false,
Reasoning = reasoning,
TotalTokens = totalTokens,
Warnings = ["Agent did not call emit_conclusion"]
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Anomaly detection failed for monitor '{MonitorName}'", spec.Name);
return AnomalyDetectionResult.Failed(ex.Message);
}
}
private static AITool CreateEmitConclusionTool(ConclusionHolder holder)
{
return AIFunctionFactory.Create(
(bool anomalyDetected, string? severity, string summary,
string detailedAnalysis, string? findingsJson) =>
{
EventSeverity? parsedSeverity = null;
if (!string.IsNullOrEmpty(severity) &&
Enum.TryParse<EventSeverity>(severity, true, out var sev))
parsedSeverity = sev;
var findings = ParseFindings(findingsJson);
holder.Result = new AnomalyDetectionResult
{
AnomalyDetected = anomalyDetected,
Severity = parsedSeverity,
Summary = summary,
DetailedAnalysis = detailedAnalysis,
Findings = findings
};
return anomalyDetected
? $"Conclusion recorded: {severity} severity - {summary}"
: $"Conclusion recorded: No anomaly - {summary}";
},
name: "emit_conclusion",
description: "Report your final analysis conclusion with findings");
}
private static string BuildReasoningTrace(ChatResponse response)
{
var lines = new List<string>();
foreach (var message in response.Messages)
{
if (message.Role == ChatRole.Assistant)
{
// Capture text content
var text = message.Text;
if (!string.IsNullOrWhiteSpace(text))
lines.Add($"[Assistant] {text}");
// Capture tool calls
foreach (var call in message.Contents.OfType<FunctionCallContent>())
lines.Add($"[Tool Call] {call.Name}({TruncateArgs(call.Arguments)})");
}
else if (message.Role == ChatRole.Tool)
{
// Capture tool results (truncated)
foreach (var result in message.Contents.OfType<FunctionResultContent>())
{
var resultText = result.Result?.ToString() ?? "";
var truncated = resultText.Length > 500
? resultText[..500] + "..."
: resultText;
lines.Add($"[Tool Result] {result.CallId}: {truncated}");
}
}
}
return string.Join("\n", lines);
}
private static List<AnomalyFinding> ParseFindings(string? findingsJson)
{
if (string.IsNullOrEmpty(findingsJson)) return [];
try
{
return JsonSerializer.Deserialize<List<AnomalyFinding>>(findingsJson) ?? [];
}
catch
{
return [];
}
}
private static string TruncateArgs(IDictionary<string, object?>? args)
{
if (args is null) return "{}";
var json = JsonSerializer.Serialize(args);
return json.Length > 200 ? json[..200] + "..." : json;
}
}Key Implementation Details
UseFunctionInvocation() Middleware (NOT in this file)
The UseFunctionInvocation() middleware is registered on the IChatClient pipeline in DI (see Files to Modify below and TASK 5.2 for full DI wiring). It wraps the inner chat client with FunctionInvokingChatClient which:
- Automatically invokes tools when the LLM requests them
- Feeds tool results back to the LLM
- Loops until the LLM produces a final text response (no more tool calls)
- Has a configurable
MaximumIterationsPerRequest(default is sufficient; can be tuned if needed)
This means the agent itself is simple - one GetResponseAsync call does all the work.
ConclusionHolder Pattern
The emit_conclusion tool needs to capture a result that the agent can read after GetResponseAsync returns. Since closures cannot capture ref parameters, we use a simple mutable wrapper class (ConclusionHolder) that the closure writes to and the agent reads from.
emit_conclusion Tool
This is the only tool we define in code. It provides a structured way for the LLM to report results that map to AnomalyDetectionResult. The system prompt in MonitorSpec should instruct the agent to use this tool.
Reasoning Trace from ChatResponse
With UseFunctionInvocation(), the returned ChatResponse contains the full conversation history (all messages including assistant tool calls and tool results). BuildReasoningTrace extracts this into a human-readable log for debugging.
No Domain Logic in Code
- No prep-time specific code
- No query builders
- No typed DTOs for query results
- The LLM handles all data interpretation
Files to Modify
3. backend/Kds.Ai.AgentsCommon/DependencyInjectionExtensions.cs
Add UseFunctionInvocation() to the IChatClient pipeline. This must be added here (not in AnomalyDetector module) because AgentsCommon is where AddChatClient is called:
public static IHostApplicationBuilder AddAgentsCommonModule(this IHostApplicationBuilder builder)
{
builder.AddOptions<AwsBedrockCredentials>();
builder.Services.AddChatClient(provider =>
{
var settings = provider.GetRequiredService<IOptions<AwsBedrockCredentials>>();
var accessKey = settings.Value.AccessKeyId;
var secretKey = settings.Value.SecretAccessKey;
var runtime = new AmazonBedrockRuntimeClient(accessKey, secretKey, RegionEndpoint.USWest2);
var modelId = settings.Value.ModelId;
var client = runtime.AsIChatClient(modelId);
return client;
})
.UseFunctionInvocation(); // <-- ADD THIS: enables automatic tool calling for all agents
return builder;
}Note: AddChatClient returns a ChatClientBuilder, so UseFunctionInvocation() chains directly. This enables automatic tool invocation for any agent that injects IChatClient.
4. backend/Kds.Ai.AnomalyDetector/DependencyInjectionExtensions.cs
Register the agent:
builder.Services.AddScoped<IAnomalyDetectorAgent, AnomalyDetectorAgent>();Acceptance Criteria
- [ ]
AnomalyDetectorAgentimplementsIAnomalyDetectorAgent - [ ] Agent receives MCP tools from
McpToolProvider - [ ] Agent adds
emit_conclusiontool for structured output - [ ]
UseFunctionInvocation()middleware registered onIChatClientpipeline inAgentsCommon - [ ] Single
GetResponseAsynccall (no manual agentic loop) - [ ]
ConclusionHolderwrapper used instead ofrefparameter - [ ] Token usage extracted from response
- [ ] Reasoning trace built from full
ChatResponse.Messagesconversation history - [ ] Error handling prevents crashes
- [ ] No domain-specific (prep-time, KDSDev) code
- [ ] Solution builds without errors
Notes
- The
FunctionInvokingChatClienthas a defaultMaximumIterationsPerRequestthat should be sufficient. If agents hit the limit, it can be configured via theconfigurecallback onUseFunctionInvocation(). ChatResponse.Messagescontains the full multi-turn conversation afterUseFunctionInvocation()resolves all tool calls. This is how we build the reasoning trace.ChatResponse.Usageaggregates token usage across all iterations handled by the middleware.- MonitorSpec.SystemPrompt should instruct the agent to call
emit_conclusionwhen done.