diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 8a0d703..83e1099 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -438,6 +438,13 @@ export default function App({ const agent = await client.agents.retrieve(agentId); setLlmConfig(agent.llm_config); setAgentName(agent.name); + + // Detect current toolset from attached tools + const { detectToolsetFromAgent } = await import("../tools/toolset"); + const detected = await detectToolsetFromAgent(client, agentId); + if (detected) { + setCurrentToolset(detected); + } } catch (error) { console.error("Error fetching agent config:", error); } diff --git a/src/index.ts b/src/index.ts index 2957699..e891a2f 100755 --- a/src/index.ts +++ b/src/index.ts @@ -360,11 +360,77 @@ async function main() { useEffect(() => { async function init() { setLoadingState("assembling"); - const modelForTools = getModelForToolLoading(model, toolset); - await loadTools(modelForTools); + const client = await getClient(); + + // Determine which agent we'll be using (before loading tools) + let resumingAgentId: string | null = null; + + // Priority 1: --agent flag + if (agentIdArg) { + try { + await client.agents.retrieve(agentIdArg); + resumingAgentId = agentIdArg; + } catch { + // Agent doesn't exist, will create new later + } + } + + // Priority 2: Skip resume if --new flag + if (!resumingAgentId && !forceNew) { + // Priority 3: Try project settings + await settingsManager.loadLocalProjectSettings(); + const localProjectSettings = + settingsManager.getLocalProjectSettings(); + if (localProjectSettings?.lastAgent) { + try { + await client.agents.retrieve(localProjectSettings.lastAgent); + resumingAgentId = localProjectSettings.lastAgent; + } catch { + // Agent no longer exists + } + } + + // Priority 4: Try global settings if --continue flag + if (!resumingAgentId && continueSession && settings.lastAgent) { + try { + await client.agents.retrieve(settings.lastAgent); + resumingAgentId = settings.lastAgent; + } catch { + // Agent no longer exists + } + } + } + + // If resuming an existing agent, load the exact tools attached to it + // Otherwise, load a full toolset based on model/toolset preference + if (resumingAgentId && !toolset) { + try { + const { getAttachedLettaTools } = await import("./tools/toolset"); + const { loadSpecificTools } = await import("./tools/manager"); + const attachedTools = await getAttachedLettaTools( + client, + resumingAgentId, + ); + if (attachedTools.length > 0) { + // Load only the specific tools attached to this agent + await loadSpecificTools(attachedTools); + } else { + // No Letta Code tools attached, load default based on model + const modelForTools = getModelForToolLoading(model, undefined); + await loadTools(modelForTools); + } + } catch { + // Detection failed, use model-based default + const modelForTools = getModelForToolLoading(model, undefined); + await loadTools(modelForTools); + } + } else { + // Creating new agent or explicit toolset specified - load full toolset + const modelForTools = getModelForToolLoading(model, toolset); + await loadTools(modelForTools); + } setLoadingState("upserting"); - const client = await getClient(); await upsertToolsToServer(client); // Handle --link/--unlink after upserting tools diff --git a/src/tools/manager.ts b/src/tools/manager.ts index 3968171..0f89bc5 100644 --- a/src/tools/manager.ts +++ b/src/tools/manager.ts @@ -215,6 +215,43 @@ export async function analyzeToolApproval( return analyzeApprovalContext(toolName, toolArgs, workingDirectory); } +/** + * Loads specific tools by name into the registry. + * Used when resuming an agent to load only the tools attached to that agent. + * + * @param toolNames - Array of specific tool names to load + */ +export async function loadSpecificTools(toolNames: string[]): Promise { + for (const name of toolNames) { + // Skip if tool filter is active and this tool is not enabled + const { toolFilter } = await import("./filter"); + if (!toolFilter.isEnabled(name)) { + continue; + } + + const definition = TOOL_DEFINITIONS[name as ToolName]; + if (!definition) { + console.warn(`Tool ${name} not found in definitions, skipping`); + continue; + } + + if (!definition.impl) { + throw new Error(`Tool implementation not found for ${name}`); + } + + const toolSchema: ToolSchema = { + name, + description: definition.description, + input_schema: definition.schema, + }; + + toolRegistry.set(name, { + schema: toolSchema, + fn: definition.impl, + }); + } +} + /** * Loads all tools defined in TOOL_NAMES and constructs their full schemas + function references. * This should be called on program startup. diff --git a/src/tools/toolset.ts b/src/tools/toolset.ts index 73aa95d..fb8e857 100644 --- a/src/tools/toolset.ts +++ b/src/tools/toolset.ts @@ -1,3 +1,4 @@ +import type Letta from "@letta-ai/letta-client"; import { getClient } from "../agent/client"; import { resolveModel } from "../agent/model"; import { linkToolsToAgent, unlinkToolsFromAgent } from "../agent/modify"; @@ -10,6 +11,81 @@ import { upsertToolsToServer, } from "./manager"; +const CODEX_TOOLS = [ + "shell_command", + "shell", + "read_file", + "list_dir", + "grep_files", + "apply_patch", + "update_plan", +]; + +const ANTHROPIC_TOOLS = [ + "Bash", + "BashOutput", + "Edit", + "ExitPlanMode", + "Glob", + "Grep", + "KillBash", + "LS", + "MultiEdit", + "Read", + "TodoWrite", + "Write", +]; + +/** + * Gets the list of Letta Code tools currently attached to an agent. + * Returns the tool names that are both attached to the agent AND in our tool definitions. + */ +export async function getAttachedLettaTools( + client: Letta, + agentId: string, +): Promise { + const agent = await client.agents.retrieve(agentId, { + include: ["agent.tools"], + }); + + const toolNames = + agent.tools + ?.map((t) => t.name) + .filter((name): name is string => typeof name === "string") || []; + + // Get all possible Letta Code tool names + const allLettaTools = [...CODEX_TOOLS, ...ANTHROPIC_TOOLS]; + + // Return intersection: tools that are both attached AND in our definitions + return toolNames.filter((name) => allLettaTools.includes(name)); +} + +/** + * Detects which toolset is attached to an agent by examining its tools. + * Returns "codex" if majority are codex tools, "default" if majority are anthropic tools, + * or null if no Letta Code tools are detected. + */ +export async function detectToolsetFromAgent( + client: Letta, + agentId: string, +): Promise<"codex" | "default" | null> { + const attachedTools = await getAttachedLettaTools(client, agentId); + + if (attachedTools.length === 0) { + return null; + } + + const codexCount = attachedTools.filter((name) => + CODEX_TOOLS.includes(name), + ).length; + const anthropicCount = attachedTools.filter((name) => + ANTHROPIC_TOOLS.includes(name), + ).length; + + // Return whichever has more tools attached + return codexCount > anthropicCount ? "codex" : "default"; +} + /** * Force switch to a specific toolset regardless of model. *