Ctrl K
ComunidadGuías…

internal/app/tool_catalog.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package app

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/yeomyeonggeori/blueclaw/internal/agentruntime"
	"github.com/yeomyeonggeori/blueclaw/internal/approvalgate"
	"github.com/yeomyeonggeori/blueclaw/internal/approvalreply"
	"github.com/yeomyeonggeori/blueclaw/internal/capability"
	"github.com/yeomyeonggeori/blueclaw/internal/config"
	"github.com/yeomyeonggeori/blueclaw/internal/mcpserver"
	"github.com/yeomyeonggeori/blueclaw/internal/task"
	"github.com/yeomyeonggeori/blueprotocol/model"
	"github.com/yeomyeonggeori/blueprotocol/toolcontract"
)

type toolCatalogEndpoint struct {
	resolver     *mcpserver.SessionTokenRequesterResolver
	handler      http.Handler
	approvalGate *approvalgate.Gate
	replyReader  approvalreply.DecisionModelReader
	scriptHost   *agentruntime.ScriptHost
}

func newToolCatalogEndpoint(taskRunService *task.TaskRunService, newQuestionWorder QuestionWorderFactory, approvalLanguageModel model.LanguageModelProvider, decisionModel model.DecisionModel, capabilityClient capability.Client) toolCatalogEndpoint {
	resolver := mcpserver.NewSessionTokenRequesterResolver(newToolCatalogSessionToken)
	handler := mcpserver.NewToolCatalogHandler(resolver, "1")
	approvalGate := approvalgate.New(taskRunService)
	if newQuestionWorder != nil {
		approvalGate.UseQuestionWorder(newQuestionWorder(approvalLanguageModel))
	}
	approvalGate.UseApprovalTargetResolver(agentruntime.NewCapabilityApprovalTargetResolver(capabilityClient))
	return toolCatalogEndpoint{resolver: resolver, handler: handler, approvalGate: approvalGate, replyReader: approvalreply.NewDecisionModelReader(decisionModel), scriptHost: agentruntime.NewScriptHost()}
}

func newToolCatalogBuilder(runtimeConfiguration config.RuntimeConfiguration, kernel agentKernel, services taskServices, directory identityDirectory, memoryComponents memoryComponents, logger *slog.Logger) *agentruntime.ToolCatalogBuilder {
	logger.Info("application.initializing", "stage", "tool_catalog")
	toolCatalogBuilder := agentruntime.NewToolCatalogBuilder()
	toolCatalogBuilder.UseCapabilityQuarantineReporter(func(quarantinedProvider toolcontract.QuarantinedToolProvider) {
		logCapabilityProviderQuarantine(logger, quarantinedProvider)
	})
	toolCatalogBuilder.UseCapabilityToolShadowReporter(func(shadowedTool agentruntime.ShadowedCapabilityTool) {
		logShadowedCapabilityTool(logger, shadowedTool)
	})
	toolCatalogBuilder.UseCapabilityRegistry(kernel.capabilityClient, kernel.capabilityRegistry)
	toolCatalogBuilder.UseRecordCatalogDivergenceReporter(func(divergence agentruntime.RecordCatalogDivergence) {
		logRecordCatalogDivergence(logger, divergence)
	})
	warmCapabilityRegistry(kernel.capabilityRegistry)
	toolCatalogBuilder.UseAllowedToolNamesByProfile(deriveAllowedToolNamesByProfile(runtimeConfiguration), agentruntime.AlwaysAllowedToolNames())
	toolCatalogBuilder.UseSkillSearch(kernel.skillRetriever, kernel.instructionBundleLoader)
	toolCatalogBuilder.UseTerminalService(kernel.terminalService)
	toolCatalogBuilder.UseTaskRunService(services.taskRunService)
	toolCatalogBuilder.UseTaskArtifactService(services.taskArtifactService)
	toolCatalogBuilder.UseWorkspaceRootPath(runtimeConfiguration.Terminal.WorkspaceRootPath)
	toolCatalogBuilder.UseSkillChangeHandler(kernel.refreshSkillIndex)
	toolCatalogBuilder.UseScriptHost(kernel.toolCatalog.scriptHost, scriptHostURL(runtimeConfiguration))
	toolCatalogBuilder.UseScriptModels(kernel.decisionModel, newConfiguredVisualReviewModel(runtimeConfiguration, logger), kernel.taskTierLanguageModels.Medium)
	if memoryComponents.stores != nil {
		toolCatalogBuilder.UseMemoryStores(memoryComponents.stores, directory.identityService)
	}
	return toolCatalogBuilder
}

func logCapabilityProviderQuarantine(logger *slog.Logger, quarantinedProvider toolcontract.QuarantinedToolProvider) {
	if logger == nil {
		return
	}
	logger.Warn("capability.provider.quarantined", "providerID", quarantinedProvider.ProviderID, "reason", quarantinedProvider.Reason)
}

func logShadowedCapabilityTool(logger *slog.Logger, shadowedTool agentruntime.ShadowedCapabilityTool) {
	if logger == nil {
		return
	}
	logger.Warn("capability.tool.shadowed",
		"canonicalName", shadowedTool.CanonicalName,
		"modelName", shadowedTool.ModelName,
	)
}

func logRecordCatalogDivergence(logger *slog.Logger, divergence agentruntime.RecordCatalogDivergence) {
	if logger == nil {
		return
	}
	logger.Warn("record.catalog.divergence",
		"requesterEmail", divergence.RequesterEmail,
		"discoveredOnly", divergence.DiscoveredOnly,
		"stampedOnly", divergence.StampedOnly,
		"discoveryFailed", divergence.DiscoveryFailed,
	)
}

func warmCapabilityRegistry(capabilityRegistry *agentruntime.CapabilityRegistry) {
	warmContext, cancelWarm := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancelWarm()
	capabilityRegistry.Warm(warmContext)
}

func capabilityToolDescriptors(toolDescriptors []config.CapabilityToolDescriptor) []agentruntime.CapabilityToolDescriptor {
	catalogToolDescriptors := []agentruntime.CapabilityToolDescriptor{}
	for _, toolDescriptor := range toolDescriptors {
		toolDescriptor.Name = strings.TrimSpace(toolDescriptor.Name)
		toolDescriptor.Description = strings.TrimSpace(toolDescriptor.Description)
		if toolDescriptor.Name == "" {
			continue
		}
		catalogToolDescriptors = append(catalogToolDescriptors, toolDescriptor)
	}
	return catalogToolDescriptors
}

func capabilityCompletionEvidence(completionEvidence *config.CapabilityCompletionEvidence) *agentruntime.CapabilityCompletionEvidence {
	if completionEvidence == nil {
		return nil
	}
	return &agentruntime.CapabilityCompletionEvidence{
		Mode:       completionEvidence.Mode,
		Action:     completionEvidence.Action,
		TargetKind: completionEvidence.TargetKind,
	}
}

func deriveAllowedToolNamesByProfile(runtimeConfiguration config.RuntimeConfiguration) map[string][]string {
	allowedToolNamesByProfile := map[string][]string{}
	for _, agentProfile := range runtimeConfiguration.AgentProfiles {
		profileName := strings.TrimSpace(agentProfile.Name)
		if profileName == "" {
			profileName = "default"
		}
		allowedToolNamesByProfile[profileName] = appendDefaultBuiltInToolNames(agentProfile.AllowedToolNames)
	}
	return allowedToolNamesByProfile
}

func appendDefaultBuiltInToolNames(toolNames []string) []string {
	allowedToolNames := agentruntime.AlwaysAllowedToolNames()
	for _, toolName := range toolNames {
		trimmedToolName := strings.TrimSpace(toolName)
		if trimmedToolName == "" || containsString(allowedToolNames, trimmedToolName) {
			continue
		}
		allowedToolNames = append(allowedToolNames, trimmedToolName)
	}
	return allowedToolNames
}

func containsString(values []string, expectedValue string) bool {
	for _, value := range values {
		if strings.TrimSpace(value) == expectedValue {
			return true
		}
	}
	return false
}

func newToolCatalogSessionToken() string {
	sessionToken := make([]byte, 32)
	if _, errorValue := rand.Read(sessionToken); errorValue != nil {
		return ""
	}
	return hex.EncodeToString(sessionToken)
}

func scriptHostURL(runtimeConfiguration config.RuntimeConfiguration) string {
	return "http://" + deriveListenAddress(runtimeConfiguration.BaseURL) + agentruntime.ScriptHostPath
}

func toolCatalogURL(runtimeConfiguration config.RuntimeConfiguration) string {
	if configuredURL := strings.TrimSpace(runtimeConfiguration.Agent.Harness.ToolCatalogURL); configuredURL != "" {
		return configuredURL
	}
	return "http://" + deriveListenAddress(runtimeConfiguration.BaseURL) + "/harness/tool-catalog"
}