Download packages/core/src/hooks/hookAggregator.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/hooks/hookAggregator.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/hooks/hookAggregator.ts
-
curl -L -o hookAggregator.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/hooks/hookAggregator.ts
10.4 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { FunctionCallingConfigMode } from '@google/genai'; | |
| import { | |
| DefaultHookOutput, | |
| BeforeToolHookOutput, | |
| BeforeModelHookOutput, | |
| BeforeToolSelectionHookOutput, | |
| AfterModelHookOutput, | |
| AfterAgentHookOutput, | |
| HookEventName, | |
| type HookOutput, | |
| type HookExecutionResult, | |
| type BeforeToolSelectionOutput, | |
| } from './types.js'; | |
| /** | |
| * Aggregated hook result | |
| */ | |
| export interface AggregatedHookResult { | |
| success: boolean; | |
| finalOutput?: DefaultHookOutput; | |
| allOutputs: HookOutput[]; | |
| errors: Error[]; | |
| totalDuration: number; | |
| } | |
| /** | |
| * Hook aggregator that merges results from multiple hooks using event-specific strategies | |
| */ | |
| export class HookAggregator { | |
| /** | |
| * Aggregate results from multiple hook executions | |
| */ | |
| aggregateResults( | |
| results: HookExecutionResult[], | |
| eventName: HookEventName, | |
| ): AggregatedHookResult { | |
| const allOutputs: HookOutput[] = []; | |
| const errors: Error[] = []; | |
| let totalDuration = 0; | |
| // Collect all outputs and errors | |
| for (const result of results) { | |
| totalDuration += result.duration; | |
| if (result.error) { | |
| errors.push(result.error); | |
| } | |
| if (result.output) { | |
| allOutputs.push(result.output); | |
| } | |
| } | |
| // Merge outputs using event-specific strategy | |
| const mergedOutput = this.mergeOutputs(allOutputs, eventName); | |
| const finalOutput = mergedOutput | |
| ? this.createSpecificHookOutput(mergedOutput, eventName) | |
| : undefined; | |
| return { | |
| success: errors.length === 0, | |
| finalOutput, | |
| allOutputs, | |
| errors, | |
| totalDuration, | |
| }; | |
| } | |
| /** | |
| * Merge hook outputs using event-specific strategies | |
| * | |
| * Note: We always use the merge logic even for single hooks to ensure | |
| * consistent default behaviors (e.g., default decision='allow' for OR logic) | |
| */ | |
| private mergeOutputs( | |
| outputs: HookOutput[], | |
| eventName: HookEventName, | |
| ): HookOutput | undefined { | |
| if (outputs.length === 0) { | |
| return undefined; | |
| } | |
| switch (eventName) { | |
| case HookEventName.BeforeTool: | |
| case HookEventName.AfterTool: | |
| case HookEventName.BeforeAgent: | |
| case HookEventName.AfterAgent: | |
| case HookEventName.SessionStart: | |
| return this.mergeWithOrDecision(outputs); | |
| case HookEventName.BeforeModel: | |
| case HookEventName.AfterModel: | |
| return this.mergeWithFieldReplacement(outputs); | |
| case HookEventName.BeforeToolSelection: | |
| return this.mergeToolSelectionOutputs( | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| outputs as BeforeToolSelectionOutput[], | |
| ); | |
| default: | |
| // For other events, use simple merge | |
| return this.mergeSimple(outputs); | |
| } | |
| } | |
| /** | |
| * Merge outputs with OR decision logic and message concatenation | |
| */ | |
| private mergeWithOrDecision(outputs: HookOutput[]): HookOutput { | |
| const merged: HookOutput = { | |
| continue: true, | |
| suppressOutput: false, | |
| }; | |
| const messages: string[] = []; | |
| const reasons: string[] = []; | |
| const systemMessages: string[] = []; | |
| const additionalContexts: string[] = []; | |
| let hasBlockDecision = false; | |
| let hasAskDecision = false; | |
| let hasContinueFalse = false; | |
| for (const output of outputs) { | |
| // Handle continue flag | |
| if (output.continue === false) { | |
| hasContinueFalse = true; | |
| merged.continue = false; | |
| if (output.stopReason) { | |
| messages.push(output.stopReason); | |
| } | |
| } | |
| // Handle decision (OR logic for blocking) | |
| const tempOutput = new DefaultHookOutput(output); | |
| if (tempOutput.isBlockingDecision()) { | |
| hasBlockDecision = true; | |
| merged.decision = output.decision; | |
| } else if (tempOutput.isAskDecision()) { | |
| hasAskDecision = true; | |
| // Ask decision is only set if no blocking decision was found so far | |
| if (!hasBlockDecision) { | |
| merged.decision = output.decision; | |
| } | |
| } | |
| // Collect messages | |
| if (output.reason) { | |
| reasons.push(output.reason); | |
| } | |
| if (output.systemMessage) { | |
| systemMessages.push(output.systemMessage); | |
| } | |
| // Handle suppress output (any true wins) | |
| if (output.suppressOutput) { | |
| merged.suppressOutput = true; | |
| } | |
| // Handle clearContext (any true wins) - for AfterAgent hooks | |
| if (output.hookSpecificOutput?.['clearContext'] === true) { | |
| merged.hookSpecificOutput = { | |
| ...(merged.hookSpecificOutput || {}), | |
| clearContext: true, | |
| }; | |
| } | |
| // Merge hookSpecificOutput (excluding clearContext which is handled above) | |
| if (output.hookSpecificOutput) { | |
| const { clearContext: _clearContext, ...restSpecificOutput } = | |
| output.hookSpecificOutput; | |
| merged.hookSpecificOutput = { | |
| ...(merged.hookSpecificOutput || {}), | |
| ...restSpecificOutput, | |
| }; | |
| } | |
| // Collect additional context from hook-specific outputs | |
| this.extractAdditionalContext(output, additionalContexts); | |
| } | |
| // Set final decision if no blocking or ask decision was found | |
| if (!hasBlockDecision && !hasAskDecision && !hasContinueFalse) { | |
| merged.decision = 'allow'; | |
| } | |
| // Merge messages | |
| if (messages.length > 0) { | |
| merged.stopReason = messages.join('\n'); | |
| } | |
| if (reasons.length > 0) { | |
| merged.reason = reasons.join('\n'); | |
| } | |
| if (systemMessages.length > 0) { | |
| merged.systemMessage = systemMessages.join('\n'); | |
| } | |
| // Add merged additional context | |
| if (additionalContexts.length > 0) { | |
| merged.hookSpecificOutput = { | |
| ...(merged.hookSpecificOutput || {}), | |
| additionalContext: additionalContexts.join('\n'), | |
| }; | |
| } | |
| return merged; | |
| } | |
| /** | |
| * Merge outputs with later fields replacing earlier fields | |
| */ | |
| private mergeWithFieldReplacement(outputs: HookOutput[]): HookOutput { | |
| let merged: HookOutput = {}; | |
| for (const output of outputs) { | |
| // Later outputs override earlier ones | |
| merged = { | |
| ...merged, | |
| ...output, | |
| hookSpecificOutput: { | |
| ...merged.hookSpecificOutput, | |
| ...output.hookSpecificOutput, | |
| }, | |
| }; | |
| } | |
| return merged; | |
| } | |
| /** | |
| * Merge tool selection outputs with specific logic for tool config | |
| * | |
| * Tool Selection Strategy: | |
| * - The intent is to provide a UNION of tools from all hooks | |
| * - If any hook specifies NONE mode, no tools are available (most restrictive wins) | |
| * - If any hook specifies ANY mode (and no NONE), ANY mode is used | |
| * - Otherwise AUTO mode is used | |
| * - Function names are collected from all hooks and sorted for deterministic caching | |
| * | |
| * This means hooks can only add/enable tools, not filter them out individually. | |
| * If one hook restricts and another re-enables, the union takes the re-enabled tool. | |
| */ | |
| private mergeToolSelectionOutputs( | |
| outputs: BeforeToolSelectionOutput[], | |
| ): BeforeToolSelectionOutput { | |
| const merged: BeforeToolSelectionOutput = {}; | |
| const allFunctionNames = new Set<string>(); | |
| let hasNoneMode = false; | |
| let hasAnyMode = false; | |
| for (const output of outputs) { | |
| const toolConfig = output.hookSpecificOutput?.toolConfig; | |
| if (!toolConfig) { | |
| continue; | |
| } | |
| // Check mode (using simplified HookToolConfig format) | |
| if (toolConfig.mode === 'NONE') { | |
| hasNoneMode = true; | |
| } else if (toolConfig.mode === 'ANY') { | |
| hasAnyMode = true; | |
| } | |
| // Collect function names (union of all hooks) | |
| if (toolConfig.allowedFunctionNames) { | |
| for (const name of toolConfig.allowedFunctionNames) { | |
| allFunctionNames.add(name); | |
| } | |
| } | |
| } | |
| // Determine final mode and function names | |
| let finalMode: FunctionCallingConfigMode; | |
| let finalFunctionNames: string[] = []; | |
| if (hasNoneMode) { | |
| // NONE mode wins - most restrictive | |
| finalMode = FunctionCallingConfigMode.NONE; | |
| finalFunctionNames = []; | |
| } else if (hasAnyMode) { | |
| // ANY mode if present (and no NONE) | |
| finalMode = FunctionCallingConfigMode.ANY; | |
| // Sort for deterministic output to ensure consistent caching | |
| finalFunctionNames = Array.from(allFunctionNames).sort(); | |
| } else { | |
| // Default to AUTO mode | |
| finalMode = FunctionCallingConfigMode.AUTO; | |
| // Sort for deterministic output to ensure consistent caching | |
| finalFunctionNames = Array.from(allFunctionNames).sort(); | |
| } | |
| merged.hookSpecificOutput = { | |
| hookEventName: 'BeforeToolSelection', | |
| toolConfig: { | |
| mode: finalMode, | |
| allowedFunctionNames: finalFunctionNames, | |
| }, | |
| }; | |
| return merged; | |
| } | |
| /** | |
| * Simple merge for events without special logic | |
| */ | |
| private mergeSimple(outputs: HookOutput[]): HookOutput { | |
| let merged: HookOutput = {}; | |
| for (const output of outputs) { | |
| merged = { ...merged, ...output }; | |
| } | |
| return merged; | |
| } | |
| /** | |
| * Create the appropriate specific hook output class based on event type | |
| */ | |
| private createSpecificHookOutput( | |
| output: HookOutput, | |
| eventName: HookEventName, | |
| ): DefaultHookOutput { | |
| switch (eventName) { | |
| case HookEventName.BeforeTool: | |
| return new BeforeToolHookOutput(output); | |
| case HookEventName.BeforeModel: | |
| return new BeforeModelHookOutput(output); | |
| case HookEventName.BeforeToolSelection: | |
| return new BeforeToolSelectionHookOutput(output); | |
| case HookEventName.AfterModel: | |
| return new AfterModelHookOutput(output); | |
| case HookEventName.AfterAgent: | |
| return new AfterAgentHookOutput(output); | |
| default: | |
| return new DefaultHookOutput(output); | |
| } | |
| } | |
| /** | |
| * Extract additional context from hook-specific outputs | |
| */ | |
| private extractAdditionalContext( | |
| output: HookOutput, | |
| contexts: string[], | |
| ): void { | |
| const specific = output.hookSpecificOutput; | |
| if (!specific) { | |
| return; | |
| } | |
| // Extract additionalContext from various hook types | |
| if ( | |
| 'additionalContext' in specific && | |
| // eslint-disable-next-line no-restricted-syntax | |
| typeof specific['additionalContext'] === 'string' | |
| ) { | |
| contexts.push(specific['additionalContext']); | |
| } | |
| } | |
| } | |