feat: cache init tool upserts (#217)

This commit is contained in:
Ari Webb
2025-12-15 15:33:43 -08:00
committed by GitHub
parent 7ce41e52f4
commit bcb1848b9a
3 changed files with 58 additions and 3 deletions

View File

@@ -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) {

View File

@@ -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<string, string>;
}
export interface ProjectSettings {

View File

@@ -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<void> {
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<boolean> {
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