feat(ai): Log command executions and show remediation history in prompts

Phase 4 - Remediation logging integration:

1. logRemediation hook after tool execution:
   - Only logs run_command tools (main remediation action)
   - Records resourceID, resourceType, findingID
   - Extracts problem summary from user prompt
   - Truncates output for storage (max 1000 chars)
   - Distinguishes automatic (patrol) vs manual (chat) actions

2. buildRemediationContext for system prompts:
   - Shows 'Past Successful Fixes for Similar Issues' section
   - Uses keyword matching to find relevant past fixes
   - Shows 'Remediation History for This Resource' section
   - Includes timestamps and outcomes

This enables the AI to say things like:
- 'This worked before: apt clean to free 6GB (resolved)'
- 'Last time on this resource: restarted nginx (resolved)'

All tests passing.
This commit is contained in:
rcourtman 2025-12-12 14:02:14 +00:00
parent 97e049a373
commit 61206ad5f7

View file

@ -1411,6 +1411,12 @@ Always execute the commands rather than telling the user how to do it.`
Type: "tool_end",
Data: ToolEndData{Name: tc.Name, Input: toolInput, Output: result, Success: execution.Success},
})
// Log to remediation log for operational memory
// Only log run_command since that's the main remediation action
if tc.Name == "run_command" {
s.logRemediation(req, toolInput, result, execution.Success)
}
}
// Truncate large results to prevent context bloat
@ -1491,6 +1497,70 @@ func (s *Service) getToolInputDisplay(tc providers.ToolCall) string {
}
}
// logRemediation logs a tool execution to the remediation log for operational memory
// This enables learning from past fix attempts
func (s *Service) logRemediation(req ExecuteRequest, command, output string, success bool) {
s.mu.RLock()
patrol := s.patrolService
s.mu.RUnlock()
if patrol == nil {
return
}
remLog := patrol.GetRemediationLog()
if remLog == nil {
return
}
// Determine outcome
outcome := OutcomeUnknown
if success {
outcome = OutcomeResolved
} else {
outcome = OutcomeFailed
}
// Extract problem from the original prompt (first 200 chars as summary)
problem := req.Prompt
if len(problem) > 200 {
problem = problem[:200] + "..."
}
// Get resource name from context if available
resourceName := ""
if req.Context != nil {
if name, ok := req.Context["name"].(string); ok {
resourceName = name
}
}
// Truncate output for storage
truncatedOutput := output
const maxOutputLen = 1000
if len(truncatedOutput) > maxOutputLen {
truncatedOutput = truncatedOutput[:maxOutputLen] + "..."
}
remLog.Log(RemediationRecord{
ResourceID: req.TargetID,
ResourceType: req.TargetType,
ResourceName: resourceName,
FindingID: req.FindingID,
Problem: problem,
Action: command,
Output: truncatedOutput,
Outcome: outcome,
Automatic: req.UseCase == "patrol", // Patrol runs are automatic
})
log.Debug().
Str("resource_id", req.TargetID).
Str("command", command).
Bool("success", success).
Msg("Logged remediation action to operational memory")
}
// hasAgentForTarget checks if we have an agent connection for the given target
func (s *Service) hasAgentForTarget(req ExecuteRequest) bool {
if s.agentServer == nil {
@ -2411,6 +2481,9 @@ This is a 3-command job. Don't over-investigate.`
} else if req.TargetID != "" {
prompt += fmt.Sprintf("\n\n## Current Focus\nYou are analyzing %s '%s'", req.TargetType, req.TargetID)
}
// Add past remediation history for this resource
prompt += s.buildRemediationContext(req.TargetID, req.Prompt)
}
// Add any provided context in a structured way
@ -2692,3 +2765,63 @@ func providerDisplayName(provider string) string {
func (s *Service) Reload() error {
return s.LoadConfig()
}
// buildRemediationContext adds past remediation history to help AI learn from previous fixes
func (s *Service) buildRemediationContext(resourceID, currentProblem string) string {
s.mu.RLock()
patrol := s.patrolService
s.mu.RUnlock()
if patrol == nil {
return ""
}
remLog := patrol.GetRemediationLog()
if remLog == nil {
return ""
}
var context string
// Get similar past remediations based on the current problem
if currentProblem != "" {
successful := remLog.GetSuccessfulRemediations(currentProblem, 3)
if len(successful) > 0 {
context += "\n\n## Past Successful Fixes for Similar Issues\n"
context += "These actions worked for similar problems before:\n"
for _, rec := range successful {
context += fmt.Sprintf("- **%s**: `%s` (%s)\n",
truncateString(rec.Problem, 60),
truncateString(rec.Action, 80),
rec.Outcome)
}
}
}
// Get history for this specific resource
if resourceID != "" {
history := remLog.GetForResource(resourceID, 5)
if len(history) > 0 {
context += "\n\n## Remediation History for This Resource\n"
for _, rec := range history {
ago := time.Since(rec.Timestamp)
agoStr := formatDuration(ago)
context += fmt.Sprintf("- %s ago: %s → `%s` (%s)\n",
agoStr,
truncateString(rec.Problem, 50),
truncateString(rec.Action, 60),
rec.Outcome)
}
}
}
return context
}
// truncateString truncates a string to maxLen characters
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen-3] + "..."
}