Add /link and /unlink commands for managing agent tools (#59)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Cameron
2025-11-05 18:11:51 -08:00
committed by GitHub
parent 424fabaed4
commit 79bc3c2d98
10 changed files with 701 additions and 4 deletions

View File

@@ -2,6 +2,7 @@
// Utilities for modifying agent configuration
import type { LlmConfig } from "@letta-ai/letta-client/resources/models/models";
import { getToolNames } from "../tools/manager";
import { getClient } from "./client";
/**
@@ -44,3 +45,149 @@ export async function updateAgentLLMConfig(
return finalConfig;
}
export interface LinkResult {
success: boolean;
message: string;
addedCount?: number;
}
export interface UnlinkResult {
success: boolean;
message: string;
removedCount?: number;
}
/**
* Attach all Letta Code tools to an agent.
*
* @param agentId - The agent ID
* @returns Result with success status and message
*/
export async function linkToolsToAgent(agentId: string): Promise<LinkResult> {
try {
const client = await getClient();
// Get ALL agent tools from agent state
const agent = await client.agents.retrieve(agentId);
const currentTools = agent.tools || [];
const currentToolIds = currentTools
.map((t) => t.id)
.filter((id): id is string => typeof id === "string");
const currentToolNames = new Set(
currentTools
.map((t) => t.name)
.filter((name): name is string => typeof name === "string"),
);
// Get Letta Code tool names
const lettaCodeToolNames = getToolNames();
// Find tools to add (tools that aren't already attached)
const toolsToAdd = lettaCodeToolNames.filter(
(name) => !currentToolNames.has(name),
);
if (toolsToAdd.length === 0) {
return {
success: true,
message: "All Letta Code tools already attached",
addedCount: 0,
};
}
// Look up tool IDs from global tool list
const toolsToAddIds: string[] = [];
for (const toolName of toolsToAdd) {
const tools = await client.tools.list({ name: toolName });
const tool = tools[0];
if (tool?.id) {
toolsToAddIds.push(tool.id);
}
}
// Combine current tools with new tools
const newToolIds = [...currentToolIds, ...toolsToAddIds];
// Get current tool_rules and add requires_approval rules for new tools
const currentToolRules = agent.tool_rules || [];
const newToolRules = [
...currentToolRules,
...toolsToAdd.map((toolName) => ({
tool_name: toolName,
type: "requires_approval" as const,
prompt_template: null,
})),
];
await client.agents.modify(agentId, {
tool_ids: newToolIds,
tool_rules: newToolRules,
});
return {
success: true,
message: `Attached ${toolsToAddIds.length} Letta Code tool(s) to agent`,
addedCount: toolsToAddIds.length,
};
} catch (error) {
return {
success: false,
message: `Failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
/**
* Remove all Letta Code tools from an agent.
*
* @param agentId - The agent ID
* @returns Result with success status and message
*/
export async function unlinkToolsFromAgent(
agentId: string,
): Promise<UnlinkResult> {
try {
const client = await getClient();
// Get ALL agent tools from agent state (not tools.list which may be incomplete)
const agent = await client.agents.retrieve(agentId);
const allTools = agent.tools || [];
const lettaCodeToolNames = new Set(getToolNames());
// Filter out Letta Code tools, keep everything else
const remainingTools = allTools.filter(
(t) => t.name && !lettaCodeToolNames.has(t.name),
);
const removedCount = allTools.length - remainingTools.length;
// Extract IDs from remaining tools (filter out any undefined IDs)
const remainingToolIds = remainingTools
.map((t) => t.id)
.filter((id): id is string => typeof id === "string");
// Remove approval rules for Letta Code tools being unlinked
const currentToolRules = agent.tool_rules || [];
const remainingToolRules = currentToolRules.filter(
(rule) =>
rule.type !== "requires_approval" ||
!lettaCodeToolNames.has(rule.tool_name),
);
await client.agents.modify(agentId, {
tool_ids: remainingToolIds,
tool_rules: remainingToolRules,
});
return {
success: true,
message: `Removed ${removedCount} Letta Code tool(s) from agent`,
removedCount,
};
} catch (error) {
return {
success: false,
message: `Failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}