diff --git a/src/index.ts b/src/index.ts index cea8f69..c908d63 100755 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import type { AgentProvenance } from "./agent/create"; import { LETTA_CLOUD_API_URL } from "./auth/oauth"; import { permissionMode } from "./permissions/mode"; import { settingsManager } from "./settings-manager"; -import { loadTools, upsertToolsToServer } from "./tools/manager"; +import { loadTools, upsertToolsIfNeeded } from "./tools/manager"; function printHelp() { // Keep this plaintext (no colors) so output pipes cleanly @@ -431,7 +431,7 @@ async function main() { ); await loadTools(modelForTools); const client = await getClient(); - await upsertToolsToServer(client); + await upsertToolsIfNeeded(client, baseURL); const { handleHeadlessCommand } = await import("./headless"); await handleHeadlessCommand(process.argv, specifiedModel, skillsDirectory); @@ -573,7 +573,7 @@ async function main() { } setLoadingState("upserting"); - await upsertToolsToServer(client); + await upsertToolsIfNeeded(client, baseURL); // Handle --link/--unlink after upserting tools if (shouldLink || shouldUnlink) { diff --git a/src/settings-manager.ts b/src/settings-manager.ts index a80600d..7e376a8 100644 --- a/src/settings-manager.ts +++ b/src/settings-manager.ts @@ -19,6 +19,8 @@ export interface Settings { refreshToken?: string; tokenExpiresAt?: number; // Unix timestamp in milliseconds deviceId?: string; + // Tool upsert cache: maps serverUrl -> hash of upserted tools + toolUpsertHashes?: Record; } export interface ProjectSettings { diff --git a/src/tools/manager.ts b/src/tools/manager.ts index d74684a..9bd96e6 100644 --- a/src/tools/manager.ts +++ b/src/tools/manager.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import type Letta from "@letta-ai/letta-client"; import { AuthenticationError, @@ -586,6 +587,58 @@ export async function upsertToolsToServer(client: Letta): Promise { await attemptUpsert(); } +/** + * Compute a hash of all currently loaded tools for cache invalidation. + * Includes tool names and schemas to detect any changes. + */ +export function computeToolsHash(): string { + const toolData = Array.from(toolRegistry.entries()) + .sort(([a], [b]) => a.localeCompare(b)) // deterministic order + .map(([name, tool]) => ({ + name, + serverName: getServerToolName(name), + schema: tool.schema, + })); + + return createHash("sha256") + .update(JSON.stringify(toolData)) + .digest("hex") + .slice(0, 16); // short hash is sufficient +} + +/** + * Upserts tools only if the tool definitions have changed since last upsert. + * Uses a hash of loaded tools cached in settings to skip redundant upserts. + * + * @param client - Letta client instance + * @param serverUrl - The server URL (used as cache key) + * @returns true if upsert was performed, false if skipped + */ +export async function upsertToolsIfNeeded( + client: Letta, + serverUrl: string, +): Promise { + const currentHash = computeToolsHash(); + + const { settingsManager } = await import("../settings-manager"); + const cachedHashes = settingsManager.getSetting("toolUpsertHashes") || {}; + + if (cachedHashes[serverUrl] === currentHash) { + // Tools unchanged, skip upsert + return false; + } + + // Perform upsert + await upsertToolsToServer(client); + + // Save new hash + settingsManager.updateSettings({ + toolUpsertHashes: { ...cachedHashes, [serverUrl]: currentHash }, + }); + + return true; +} + /** * Helper to clip tool return text to a reasonable display size * Used by UI components to truncate long responses for display