fix: don't clobber tools on resume (#118)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
72
src/index.ts
72
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user