All files / src/llm-orchestration/hooks invalid-tool-feedback.hook.ts

96.29% Statements 26/27
100% Branches 6/6
100% Functions 4/4
95.83% Lines 23/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 777x       7x 7x 7x       7x 11x     11x 11x 11x 11x             23x   23x 16x         7x       2x         2x     5x       6x   5x     5x             5x 5x           5x                  
import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common';
import { PostExecutionHook } from './post-execution-hook.interface';
import { SessionInput } from '../../core-entities';
import { PlanExecutionContext } from '../llm-orchestration.interfaces';
import { ChatService } from '../../interactive-chat/chat.service';
import { ApplicationStateService } from '../../application-state/application-state.service';
import { SessionInputsService } from '../../session-inputs/session-inputs.service';
import { CreateSessionInputDto } from '../../session-inputs/dto/session-input.dto';
 
@Injectable()
export class InvalidToolFeedbackHook implements PostExecutionHook {
  private readonly logger = new Logger(InvalidToolFeedbackHook.name);
 
  constructor(
    @Inject(forwardRef(() => ChatService))
    private readonly chatService: ChatService,
    private readonly applicationStateService: ApplicationStateService,
    private readonly sessionInputsService: SessionInputsService,
  ) {}
 
  async run(
    sessionInput: SessionInput,
    executionContext: PlanExecutionContext,
  ): Promise<void> {
    const invalidTools = executionContext.feedback.invalidToolErrors;
 
    if (!invalidTools || invalidTools.length === 0) {
      return; // No invalid tools, do nothing.
    }
 
    // Native tool calling: if toolResults exist, errors flow through role: 'tool' messages
    // The FollowUpPostExecutionHook will handle the follow-up turn with toolResults
    if (
      executionContext.toolResults &&
      executionContext.toolResults.length > 0
    ) {
      this.logger.log(
        `Found ${invalidTools.length} invalid tool(s), but native tool calling is active. ` +
          `Errors will flow through toolResults. FollowUpPostExecutionHook will handle the follow-up.`,
      );
      // Do NOT halt hooks - let FollowUpPostExecutionHook run to process toolResults
      return;
    }
 
    this.logger.log(
      `Found ${invalidTools.length} invalid tool(s). Sending feedback to AI.`,
    );
 
    const toolNames = invalidTools.map((e) => `'${e.tool_name}'`).join(', ');
 
    const feedbackMessage = `SYSTEM_FEEDBACK: The previous response included tools that are not valid. The following tool names could not be found: ${toolNames}. Please review the available tools and provide a corrected plan using only valid tool names. Do not apologize, just provide the corrected plan.`;
 
    // FIX: Create SessionInput record for proper conversation tracking
    const feedbackDto: CreateSessionInputDto = {
      user_prompt: feedbackMessage,
      execution_strategy: sessionInput.execution_strategy,
      ad_hoc_context_definition: undefined,
      context_template_id: undefined,
    };
 
    try {
      await this.sessionInputsService.create(
        sessionInput.session_id,
        feedbackDto,
      );
 
      // Halt subsequent hooks to prevent them from running, as we've already initiated a new turn.
      executionContext.flags.should_halt_hooks = true;
    } catch (error) {
      this.logger.error(
        `Failed to send invalid tool feedback to AI for session ${sessionInput.session_id}`,
        error.stack,
      );
    }
  }
}