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:
parent
97e049a373
commit
61206ad5f7
1 changed files with 133 additions and 0 deletions
|
|
@ -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] + "..."
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue