All files / src/llm-orchestration/action-handlers final.handler.ts

84% Statements 21/25
50% Branches 5/10
66.66% Functions 4/6
82.6% Lines 19/23

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 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 1297x             7x 7x 7x 7x     7x 10x                                                                   4x           2x                 2x                     2x                                 2x               5x 1x     5x 5x 5x                         5x   5x                
import { Injectable } from '@nestjs/common';
import { ActionHandler } from './action-handler.interface';
import {
  ActionExecutionResult,
  PlanExecutionContext,
  ToolMetadata,
} from '../llm-orchestration.interfaces';
import { FinalArgsDto } from './dto/final.args.dto';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { generateToolCall, generateToolCallJson } from '../../utils';
 
@Injectable()
export class FinalHandler implements ActionHandler {
  readonly toolName = 'final';
 
  getMetadata(): ToolMetadata {
    return {
      name: this.toolName,
      description: this.getDefinition(true),
      arguments: [
        {
          name: 'plain',
          type: 'string',
          description: 'A final concluding message or summary for user.',
          required: false,
        },
        {
          name: 'selections',
          type: 'string',
          description:
            'JSON array of selection groups. Each group is {"question":"...","selections":["a","b","c"]}. Renders as clickable buttons in the UI.',
          required: false,
        },
      ],
    };
  }
 
  /**
   * Generates a tool call example in the specified format.
   * @param toolCall - The tool call object to format
   * @param useJson - If true, uses JSON format; otherwise uses XML-style format
   * @returns Formatted tool call string
   */
  private generateExample(
    toolCall: Record<string, any>,
    useJson: boolean = false,
  ): string {
    return useJson
      ? generateToolCallJson(toolCall)
      : generateToolCall(toolCall);
  }
 
  getDefinition(useJsonFormat: boolean = false): string {
    const example = this.generateExample(
      {
        tool_name: this.toolName,
        plain:
          'The task is complete. I have refactored the component and added the new feature.',
      },
      useJsonFormat,
    );
 
    const selectionsExample = this.generateExample(
      {
        tool_name: this.toolName,
        plain:
          'I need some decisions before proceeding with the implementation.',
        selections:
          '[{"question":"Which API style should we use?","selections":["REST","GraphQL"]},{"question":"Which database?","selections":["Postgres","MySQL","SQLite"]}]',
      },
      useJsonFormat,
    );
 
    const definition = `
<${this.toolName}>
  Marks the end of a plan. This should be the last action in a sequence when the task is complete.
 
  Parameters:
  - "plain": (string, optional) A final concluding message or summary for the user.
  - "selections": (string, optional) JSON array of selection groups. Each group is {"question":"...","selections":["a","b","c"]}. The user will see clickable buttons for each selection. Use this when you need the user to make a choice before continuing.
 
  <example>
:${example}
  </example>
 
  <example>
${selectionsExample}
  </example>
</${this.toolName}>
`;
    return definition.trim();
  }
 
  async execute(
    args: { [key: string]: any },
    context: PlanExecutionContext,
  ): Promise<ActionExecutionResult> {
    // Normalize selections: LLMs may pass selections as a JSON array instead of a string.
    if (args.selections && typeof args.selections !== 'string') {
      args = { ...args, selections: JSON.stringify(args.selections) };
    }
 
    const validatedArgs = plainToClass(FinalArgsDto, args);
    const errors = await validate(validatedArgs);
    Iif (errors.length > 0) {
      const errorMessages = errors
        .map((err) => Object.values(err.constraints || {}).join(', '))
        .join('; ');
      return {
        status: 'FAILURE',
        summary: `Invalid arguments for ${this.toolName}.`,
        error_message: errorMessages,
        persisted_args: args,
        execution_log: { output: '', error_message: errorMessages },
      };
    }
 
    context.flags.is_final = true;
 
    return {
      status: 'SUCCESS',
      summary: 'Plan marked as final.',
      persisted_args: validatedArgs,
      execution_log: { output: 'Task completed.', error_message: '' },
    };
  }
}