fix: don't clobber tools on resume (#118)

This commit is contained in:
Charles Packer
2025-11-23 22:30:13 -08:00
committed by GitHub
parent 8cab132513
commit 4862a87fb1
4 changed files with 189 additions and 3 deletions

View File

@@ -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);
}

View File

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

View File

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

View File

@@ -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<string[]> {
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.
*