feat: add gemini tools (#120)

This commit is contained in:
Charles Packer
2025-11-24 10:50:31 -08:00
committed by GitHub
parent 43813383ac
commit f2ed25bfeb
51 changed files with 1639 additions and 64 deletions

View File

@@ -8,6 +8,44 @@ import { TOOL_DEFINITIONS, type ToolName } from "./toolDefinitions";
export const TOOL_NAMES = Object.keys(TOOL_DEFINITIONS) as ToolName[];
// Maps internal tool names to server/model-facing tool names
// This allows us to have multiple implementations (e.g., write_file_gemini, Write from Anthropic)
// that map to the same server tool name since only one toolset is active at a time
const TOOL_NAME_MAPPINGS: Partial<Record<ToolName, string>> = {
// Gemini tools - map to their original Gemini CLI names
glob_gemini: "glob",
write_todos: "write_todos",
write_file_gemini: "write_file",
replace: "replace",
search_file_content: "search_file_content",
read_many_files: "read_many_files",
read_file_gemini: "read_file",
list_directory: "list_directory",
run_shell_command: "run_shell_command",
};
/**
* Get the server-facing name for a tool (maps internal names to what the model sees)
*/
export function getServerToolName(internalName: string): string {
return TOOL_NAME_MAPPINGS[internalName as ToolName] || internalName;
}
/**
* Get the internal tool name from a server-facing name
* Used when the server sends back tool calls/approvals with server names
*/
export function getInternalToolName(serverName: string): string {
// Build reverse mapping
for (const [internal, server] of Object.entries(TOOL_NAME_MAPPINGS)) {
if (server === serverName) {
return internal;
}
}
// If not in mapping, the server name is the internal name
return serverName;
}
const ANTHROPIC_DEFAULT_TOOLS: ToolName[] = [
"Bash",
"BashOutput",
@@ -33,6 +71,18 @@ const OPENAI_DEFAULT_TOOLS: ToolName[] = [
"update_plan",
];
const GEMINI_DEFAULT_TOOLS: ToolName[] = [
"run_shell_command",
"read_file_gemini",
"list_directory",
"glob_gemini",
"search_file_content",
"replace",
"write_file_gemini",
"write_todos",
"read_many_files",
];
// Tool permissions configuration
const TOOL_PERMISSIONS: Record<ToolName, { requiresApproval: boolean }> = {
Bash: { requiresApproval: true },
@@ -54,6 +104,16 @@ const TOOL_PERMISSIONS: Record<ToolName, { requiresApproval: boolean }> = {
grep_files: { requiresApproval: false },
apply_patch: { requiresApproval: true },
update_plan: { requiresApproval: false },
// Gemini toolset
glob_gemini: { requiresApproval: false },
list_directory: { requiresApproval: false },
read_file_gemini: { requiresApproval: false },
read_many_files: { requiresApproval: false },
replace: { requiresApproval: true },
run_shell_command: { requiresApproval: true },
search_file_content: { requiresApproval: false },
write_todos: { requiresApproval: false },
write_file_gemini: { requiresApproval: true },
};
interface JsonSchema {
@@ -264,7 +324,13 @@ export async function loadTools(modelIdentifier?: string): Promise<void> {
const filterActive = toolFilter.isActive();
let baseToolNames: ToolName[];
if (!filterActive && modelIdentifier && isOpenAIModel(modelIdentifier)) {
if (!filterActive && modelIdentifier && isGeminiModel(modelIdentifier)) {
baseToolNames = GEMINI_DEFAULT_TOOLS;
} else if (
!filterActive &&
modelIdentifier &&
isOpenAIModel(modelIdentifier)
) {
baseToolNames = OPENAI_DEFAULT_TOOLS;
} else if (!filterActive) {
baseToolNames = ANTHROPIC_DEFAULT_TOOLS;
@@ -317,6 +383,20 @@ export function isOpenAIModel(modelIdentifier: string): boolean {
return modelIdentifier.startsWith("openai/");
}
export function isGeminiModel(modelIdentifier: string): boolean {
const info = getModelInfo(modelIdentifier);
if (info?.handle && typeof info.handle === "string") {
return (
info.handle.startsWith("google/") || info.handle.startsWith("google_ai/")
);
}
// Fallback: treat raw handle-style identifiers as Gemini
return (
modelIdentifier.startsWith("google/") ||
modelIdentifier.startsWith("google_ai/")
);
}
/**
* Upserts all loaded tools to the Letta server with retry logic.
* This registers Python stubs so the agent knows about the tools,
@@ -356,15 +436,18 @@ export async function upsertToolsToServer(client: Letta): Promise<void> {
// Race the upsert against the timeout
const upsertPromise = Promise.all(
Array.from(toolRegistry.entries()).map(async ([name, tool]) => {
// Get the server-facing tool name (may differ from internal name)
const serverName = TOOL_NAME_MAPPINGS[name as ToolName] || name;
const pythonStub = generatePythonStub(
name,
serverName,
tool.schema.description,
tool.schema.input_schema,
);
// Construct the full JSON schema in Letta's expected format
const fullJsonSchema = {
name,
name: serverName,
description: tool.schema.description,
parameters: tool.schema.input_schema,
};
@@ -554,7 +637,9 @@ export async function executeTool(
args: ToolArgs,
options?: { signal?: AbortSignal },
): Promise<ToolExecutionResult> {
const tool = toolRegistry.get(name);
// Map server name to internal name for registry lookup
const internalName = getInternalToolName(name);
const tool = toolRegistry.get(internalName);
if (!tool) {
return {