feat: persist toolset mode and add auto option (#1015)

This commit is contained in:
Charles Packer
2026-02-18 13:43:40 -08:00
committed by GitHub
parent bdc23932b5
commit 5fac4bc106
7 changed files with 333 additions and 230 deletions

View File

@@ -89,11 +89,10 @@ import {
analyzeToolApproval,
checkToolPermission,
executeTool,
isGeminiModel,
isOpenAIModel,
savePermissionRule,
type ToolExecutionResult,
} from "../tools/manager";
import type { ToolsetName, ToolsetPreference } from "../tools/toolset";
import { debugLog, debugWarn } from "../utils/debug";
import {
handleMcpAdd,
@@ -1239,13 +1238,7 @@ export default function App({
}
| {
type: "switch_toolset";
toolsetId:
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none";
toolsetId: ToolsetPreference;
commandId?: string;
}
| { type: "switch_system"; promptId: string; commandId?: string }
@@ -1263,15 +1256,11 @@ export default function App({
const [currentSystemPromptId, setCurrentSystemPromptId] = useState<
string | null
>("default");
const [currentToolset, setCurrentToolset] = useState<
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none"
| null
>(null);
const [currentToolset, setCurrentToolset] = useState<ToolsetName | null>(
null,
);
const [currentToolsetPreference, setCurrentToolsetPreference] =
useState<ToolsetPreference>("auto");
const [llmConfig, setLlmConfig] = useState<LlmConfig | null>(null);
const llmConfigRef = useRef(llmConfig);
useEffect(() => {
@@ -1279,7 +1268,7 @@ export default function App({
}, [llmConfig]);
const [currentModelId, setCurrentModelId] = useState<string | null>(null);
// Full model handle for API calls (e.g., "anthropic/claude-sonnet-4-5-20251101")
const [_currentModelHandle, setCurrentModelHandle] = useState<string | null>(
const [currentModelHandle, setCurrentModelHandle] = useState<string | null>(
null,
);
// Derive agentName from agentState (single source of truth)
@@ -2699,14 +2688,27 @@ export default function App({
// Store full handle for API calls (e.g., compaction)
setCurrentModelHandle(agentModelHandle || null);
// Derive toolset from agent's model (not persisted, computed on resume)
if (agentModelHandle) {
const derivedToolset = isOpenAIModel(agentModelHandle)
? "codex"
: isGeminiModel(agentModelHandle)
? "gemini"
: "default";
setCurrentToolset(derivedToolset);
const persistedToolsetPreference =
settingsManager.getToolsetPreference(agentId);
setCurrentToolsetPreference(persistedToolsetPreference);
if (persistedToolsetPreference === "auto") {
if (agentModelHandle) {
const { switchToolsetForModel } = await import(
"../tools/toolset"
);
const derivedToolset = await switchToolsetForModel(
agentModelHandle,
agentId,
);
setCurrentToolset(derivedToolset);
} else {
setCurrentToolset(null);
}
} else {
const { forceToolsetSwitch } = await import("../tools/toolset");
await forceToolsetSwitch(persistedToolsetPreference, agentId);
setCurrentToolset(persistedToolsetPreference);
}
} catch (error) {
console.error("Error fetching agent config:", error);
@@ -5156,6 +5158,13 @@ export default function App({
setAgentId(targetAgentId);
setAgentState(agent);
setLlmConfig(agent.llm_config);
const agentModelHandle =
agent.llm_config.model_endpoint_type && agent.llm_config.model
? agent.llm_config.model_endpoint_type +
"/" +
agent.llm_config.model
: (agent.llm_config.model ?? null);
setCurrentModelHandle(agentModelHandle);
setConversationId(targetConversationId);
// Ensure bootstrap reminders are re-injected on the first user turn
@@ -5290,6 +5299,13 @@ export default function App({
setAgentId(agent.id);
setAgentState(agent);
setLlmConfig(agent.llm_config);
const agentModelHandle =
agent.llm_config.model_endpoint_type && agent.llm_config.model
? agent.llm_config.model_endpoint_type +
"/" +
agent.llm_config.model
: (agent.llm_config.model ?? null);
setCurrentModelHandle(agentModelHandle);
// Reset context token tracking for new agent
resetContextHistory(contextTrackerRef.current);
@@ -9793,42 +9809,42 @@ ${SYSTEM_REMINDER_CLOSE}
// Reset context token tracking since different models have different tokenizers
resetContextHistory(contextTrackerRef.current);
setCurrentModelHandle(modelHandle);
const { isOpenAIModel, isGeminiModel } = await import(
"../tools/manager"
);
const targetToolset:
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none" = isOpenAIModel(modelHandle)
? "codex"
: isGeminiModel(modelHandle)
? "gemini"
: "default";
const persistedToolsetPreference =
settingsManager.getToolsetPreference(agentId);
let toolsetNoticeLine: string | null = null;
let toolsetName:
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none"
| null = null;
if (currentToolset !== targetToolset) {
if (persistedToolsetPreference === "auto") {
const { switchToolsetForModel } = await import("../tools/toolset");
toolsetName = await switchToolsetForModel(modelHandle, agentId);
const toolsetName = await switchToolsetForModel(
modelHandle,
agentId,
);
setCurrentToolsetPreference("auto");
setCurrentToolset(toolsetName);
toolsetNoticeLine =
"Auto toolset selected: switched to " +
toolsetName +
". Use /toolset to set a manual override.";
} else {
const { forceToolsetSwitch } = await import("../tools/toolset");
if (currentToolset !== persistedToolsetPreference) {
await forceToolsetSwitch(persistedToolsetPreference, agentId);
setCurrentToolset(persistedToolsetPreference);
}
setCurrentToolsetPreference(persistedToolsetPreference);
toolsetNoticeLine =
"Manual toolset override remains active: " +
persistedToolsetPreference +
".";
}
const autoToolsetLine = toolsetName
? `Automatically switched toolset to ${toolsetName}. Use /toolset to change back if desired.\nConsider switching to a different system prompt using /system to match.`
: null;
const outputLines = [
`Switched to ${model.label}${reasoningLevel ? ` (${reasoningLevel} reasoning)` : ""}`,
...(autoToolsetLine ? [autoToolsetLine] : []),
"Switched to " +
model.label +
(reasoningLevel ? ` (${reasoningLevel} reasoning)` : ""),
...(toolsetNoticeLine ? [toolsetNoticeLine] : []),
].join("\n");
cmd.finish(outputLines, true);
@@ -10020,16 +10036,7 @@ ${SYSTEM_REMINDER_CLOSE}
);
const handleToolsetSelect = useCallback(
async (
toolsetId:
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none",
commandId?: string | null,
) => {
async (toolsetId: ToolsetPreference, commandId?: string | null) => {
const overlayCommand = commandId
? commandRunner.getHandle(commandId, "/toolset")
: consumeOverlayCommand("toolset");
@@ -10058,20 +10065,51 @@ ${SYSTEM_REMINDER_CLOSE}
await withCommandLock(async () => {
const cmd =
overlayCommand ??
commandRunner.start(
"/toolset",
`Switching toolset to ${toolsetId}...`,
);
commandRunner.start("/toolset", "Switching toolset...");
cmd.update({
output: `Switching toolset to ${toolsetId}...`,
output: "Switching toolset...",
phase: "running",
});
try {
const { forceToolsetSwitch } = await import("../tools/toolset");
const { forceToolsetSwitch, switchToolsetForModel } = await import(
"../tools/toolset"
);
if (toolsetId === "auto") {
const modelHandle =
currentModelHandle ??
(llmConfig?.model_endpoint_type && llmConfig?.model
? `${llmConfig.model_endpoint_type}/${llmConfig.model}`
: (llmConfig?.model ?? null));
if (!modelHandle) {
throw new Error(
"Could not determine current model for auto toolset",
);
}
const derivedToolset = await switchToolsetForModel(
modelHandle,
agentId,
);
settingsManager.setToolsetPreference(agentId, "auto");
setCurrentToolsetPreference("auto");
setCurrentToolset(derivedToolset);
cmd.finish(
`Toolset mode set to auto (currently ${derivedToolset}).`,
true,
);
return;
}
await forceToolsetSwitch(toolsetId, agentId);
settingsManager.setToolsetPreference(agentId, toolsetId);
setCurrentToolsetPreference(toolsetId);
setCurrentToolset(toolsetId);
cmd.finish(`Switched toolset to ${toolsetId}`, true);
cmd.finish(
`Switched toolset to ${toolsetId} (manual override)`,
true,
);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(`Failed to switch toolset: ${errorDetails}`);
@@ -10082,7 +10120,9 @@ ${SYSTEM_REMINDER_CLOSE}
agentId,
commandRunner,
consumeOverlayCommand,
currentModelHandle,
isAgentBusy,
llmConfig,
withCommandLock,
],
);
@@ -11337,6 +11377,7 @@ Plan file path: ${planFilePath}`;
{activeOverlay === "toolset" && (
<ToolsetSelector
currentToolset={currentToolset ?? undefined}
currentPreference={currentToolsetPreference}
onSelect={handleToolsetSelect}
onCancel={closeOverlay}
/>

View File

@@ -1,6 +1,7 @@
// Import useInput from vendored Ink for bracketed paste support
import { Box, useInput } from "ink";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import type { ToolsetName, ToolsetPreference } from "../../tools/toolset";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
import { Text } from "./Text";
@@ -8,114 +9,86 @@ import { Text } from "./Text";
// Horizontal line character (matches approval dialogs)
const SOLID_LINE = "─";
type ToolsetId =
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none";
interface ToolsetOption {
id: ToolsetId;
id: ToolsetPreference;
label: string;
description: string;
tools: string[];
isFeatured?: boolean;
}
const toolsets: ToolsetOption[] = [
{
id: "auto",
label: "Auto",
description: "Auto-select based on the model",
isFeatured: true,
},
{
id: "none",
label: "None",
description: "Remove all Letta Code tools from your agent",
isFeatured: true,
},
{
id: "default",
label: "Default Tools",
description: "Toolset optimized for Claude models",
tools: [
"Bash",
"TaskOutput",
"Edit",
"Glob",
"Grep",
"LS",
"MultiEdit",
"Read",
"TodoWrite",
"Write",
],
label: "Claude toolset",
description: "Optimized for Anthropic models",
isFeatured: true,
},
{
id: "codex",
label: "Codex Tools",
description: "Toolset optimized for GPT/Codex models",
tools: [
"AskUserQuestion",
"EnterPlanMode",
"ExitPlanMode",
"Task",
"Skill",
"ShellCommand",
"ApplyPatch",
"UpdatePlan",
"ViewImage",
],
label: "Codex toolset",
description: "Optimized for GPT/Codex models",
isFeatured: true,
},
{
id: "gemini",
label: "Gemini toolset",
description: "Optimized for Google Gemini models",
isFeatured: true,
},
{
id: "codex_snake",
label: "Codex Tools (snake_case)",
description: "Toolset optimized for GPT/Codex models (snake_case)",
tools: ["shell_command", "apply_patch", "update_plan", "view_image"],
},
{
id: "gemini",
label: "Gemini Tools",
description: "Toolset optimized for Gemini models",
tools: [
"RunShellCommand",
"ReadFileGemini",
"ListDirectory",
"GlobGemini",
"SearchFileContent",
"Replace",
"WriteFileGemini",
"WriteTodos",
"ReadManyFiles",
],
isFeatured: true,
label: "Codex toolset (snake_case)",
description: "Optimized for GPT/Codex models (snake_case)",
},
{
id: "gemini_snake",
label: "Gemini Tools (snake_case)",
description: "Toolset optimized for Gemini models (snake_case)",
tools: [
"run_shell_command",
"read_file_gemini",
"list_directory",
"glob_gemini",
"search_file_content",
"replace",
"write_file_gemini",
"write_todos",
"read_many_files",
],
},
{
id: "none",
label: "None (Disable Tools)",
description: "Remove all Letta Code tools from the agent",
tools: [],
isFeatured: true,
label: "Gemini toolset (snake_case)",
description: "Optimized for Google Gemini models (snake_case)",
},
];
function formatEffectiveToolset(toolset?: ToolsetName): string {
if (!toolset) return "Unknown";
switch (toolset) {
case "default":
return "Claude";
case "codex":
return "Codex";
case "codex_snake":
return "Codex (snake_case)";
case "gemini":
return "Gemini";
case "gemini_snake":
return "Gemini (snake_case)";
case "none":
return "None";
default:
return toolset;
}
}
interface ToolsetSelectorProps {
currentToolset?: ToolsetId;
onSelect: (toolsetId: ToolsetId) => void;
currentToolset?: ToolsetName;
currentPreference?: ToolsetPreference;
onSelect: (toolsetId: ToolsetPreference) => void;
onCancel: () => void;
}
export function ToolsetSelector({
currentToolset,
currentPreference = "auto",
onSelect,
onCancel,
}: ToolsetSelectorProps) {
@@ -132,13 +105,16 @@ export function ToolsetSelector({
const visibleToolsets = useMemo(() => {
if (showAll) return toolsets;
if (featuredToolsets.length > 0) return featuredToolsets;
return toolsets.slice(0, 3);
return toolsets;
}, [featuredToolsets, showAll]);
const hasHiddenToolsets = visibleToolsets.length < toolsets.length;
const hasShowAllOption = !showAll && hasHiddenToolsets;
const canToggleShowAll = featuredToolsets.length < toolsets.length;
const totalItems = visibleToolsets.length + (hasShowAllOption ? 1 : 0);
useEffect(() => {
if (selectedIndex >= visibleToolsets.length) {
setSelectedIndex(Math.max(0, visibleToolsets.length - 1));
}
}, [selectedIndex, visibleToolsets.length]);
useInput((input, key) => {
// CTRL-C: immediately cancel
@@ -150,17 +126,16 @@ export function ToolsetSelector({
if (key.upArrow) {
setSelectedIndex((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
setSelectedIndex((prev) => Math.min(totalItems - 1, prev + 1));
setSelectedIndex((prev) =>
Math.min(visibleToolsets.length - 1, prev + 1),
);
} else if (key.return) {
if (hasShowAllOption && selectedIndex === visibleToolsets.length) {
setShowAll(true);
setSelectedIndex(0);
} else {
const selectedToolset = visibleToolsets[selectedIndex];
if (selectedToolset) {
onSelect(selectedToolset.id);
}
const selectedToolset = visibleToolsets[selectedIndex];
if (selectedToolset) {
onSelect(selectedToolset.id);
}
} else if (canToggleShowAll && (input === "a" || input === "A")) {
setShowAll((prev) => !prev);
} else if (key.escape) {
onCancel();
}
@@ -184,56 +159,43 @@ export function ToolsetSelector({
<Box flexDirection="column">
{visibleToolsets.map((toolset, index) => {
const isSelected = index === selectedIndex;
const isCurrent = toolset.id === currentToolset;
const isCurrent = toolset.id === currentPreference;
const labelText =
toolset.id === "auto"
? isCurrent
? `Auto (current - ${formatEffectiveToolset(currentToolset)})`
: "Auto"
: isCurrent
? `${toolset.label} (current)`
: toolset.label;
return (
<Box key={toolset.id} flexDirection="column" marginBottom={1}>
<Box flexDirection="row">
<Text
color={
isSelected ? colors.selector.itemHighlighted : undefined
}
>
{isSelected ? "> " : " "}
</Text>
<Text
bold={isSelected}
color={
isSelected ? colors.selector.itemHighlighted : undefined
}
>
{toolset.label}
{isCurrent && (
<Text color={colors.selector.itemCurrent}> (current)</Text>
)}
</Text>
</Box>
<Text dimColor>
{" "}
{toolset.description}
<Box key={toolset.id} flexDirection="row">
<Text
color={isSelected ? colors.selector.itemHighlighted : undefined}
>
{isSelected ? "> " : " "}
</Text>
<Text
bold={isSelected}
color={isSelected ? colors.selector.itemHighlighted : undefined}
>
{labelText}
</Text>
<Text dimColor>{` · ${toolset.description}`}</Text>
</Box>
);
})}
{hasShowAllOption && (
<Box flexDirection="row">
<Text
color={
selectedIndex === visibleToolsets.length
? colors.selector.itemHighlighted
: undefined
}
>
{selectedIndex === visibleToolsets.length ? "> " : " "}
</Text>
<Text dimColor>Show all toolsets</Text>
</Box>
)}
</Box>
{/* Footer */}
<Box marginTop={1}>
<Text dimColor>{" "}Enter select · navigate · Esc cancel</Text>
<Text dimColor>
{canToggleShowAll
? " Enter select · ↑↓ navigate · A show all · Esc cancel"
: " Enter select · ↑↓ navigate · Esc cancel"}
</Text>
</Box>
</Box>
);

View File

@@ -69,7 +69,7 @@ OPTIONS
-n, --name <name> Resume agent by name (from pinned agents, case-insensitive)
-m, --model <id> Model ID or handle (e.g., "opus-4.5" or "anthropic/claude-opus-4-5")
-s, --system <id> System prompt ID or subagent name (applies to new or existing agent)
--toolset <name> Force toolset: "codex", "default", or "gemini" (overrides model-based auto-selection)
--toolset <name> Toolset mode: "auto", "codex", "default", or "gemini" (manual values override model-based auto-selection)
-p, --prompt Headless prompt mode
--output-format <fmt> Output format for headless mode (text, json, stream-json)
Default: text
@@ -252,7 +252,7 @@ async function printInfo() {
*/
function getModelForToolLoading(
specifiedModel?: string,
specifiedToolset?: "codex" | "default" | "gemini",
specifiedToolset?: "auto" | "codex" | "default" | "gemini",
): string | undefined {
// If toolset is explicitly specified, use a dummy model from that provider
// to trigger the correct toolset loading logic
@@ -658,10 +658,11 @@ async function main(): Promise<void> {
specifiedToolset &&
specifiedToolset !== "codex" &&
specifiedToolset !== "default" &&
specifiedToolset !== "gemini"
specifiedToolset !== "gemini" &&
specifiedToolset !== "auto"
) {
console.error(
`Error: Invalid toolset "${specifiedToolset}". Must be "codex", "default", or "gemini".`,
`Error: Invalid toolset "${specifiedToolset}". Must be "auto", "codex", "default", or "gemini".`,
);
process.exit(1);
}
@@ -985,7 +986,7 @@ async function main(): Promise<void> {
// For headless mode, load tools synchronously (respecting model/toolset when provided)
const modelForTools = getModelForToolLoading(
specifiedModel,
specifiedToolset as "codex" | "default" | undefined,
specifiedToolset as "auto" | "codex" | "default" | "gemini" | undefined,
);
await loadTools(modelForTools);
markMilestone("TOOLS_LOADED");
@@ -1042,7 +1043,7 @@ async function main(): Promise<void> {
agentIdArg: string | null;
model?: string;
systemPromptPreset?: string;
toolset?: "codex" | "default" | "gemini";
toolset?: "auto" | "codex" | "default" | "gemini";
skillsDirectory?: string;
fromAfFile?: string;
isRegistryImport?: boolean;
@@ -1540,12 +1541,11 @@ async function main(): Promise<void> {
// Set resuming state early so loading messages are accurate
setIsResumingSession(!!resumingAgentId);
// Load toolset: use explicit --toolset flag if provided, otherwise derive from model
// NOTE: We don't persist toolset per-agent. On resume, toolset is re-derived from model.
// If explicit toolset overrides need to persist, see comment in tools/toolset.ts
// Load an initial toolset for startup (explicit --toolset or model-derived).
// App.tsx will reconcile persisted per-agent toolset preference after agent metadata loads.
const modelForTools = getModelForToolLoading(
model,
toolset as "codex" | "default" | undefined,
toolset as "auto" | "codex" | "default" | "gemini" | undefined,
);
await loadTools(modelForTools);
@@ -2136,7 +2136,12 @@ async function main(): Promise<void> {
agentIdArg: specifiedAgentId,
model: specifiedModel,
systemPromptPreset: systemPromptPreset,
toolset: specifiedToolset as "codex" | "default" | "gemini" | undefined,
toolset: specifiedToolset as
| "auto"
| "codex"
| "default"
| "gemini"
| undefined,
skillsDirectory: skillsDirectory,
fromAfFile: fromAfFile,
isRegistryImport: isRegistryImport,

View File

@@ -47,6 +47,14 @@ export interface AgentSettings {
baseUrl?: string; // undefined = Letta API (api.letta.com)
pinned?: boolean; // true if agent is pinned
memfs?: boolean; // true if memory filesystem is enabled
toolset?:
| "auto"
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none"; // toolset mode for this agent (manual override or auto)
}
export interface Settings {
@@ -1317,10 +1325,15 @@ class SettingsManager {
pinned: updates.pinned !== undefined ? updates.pinned : existing.pinned,
// Use nullish coalescing for memfs (undefined = keep existing)
memfs: updates.memfs !== undefined ? updates.memfs : existing.memfs,
// Use nullish coalescing for toolset (undefined = keep existing)
toolset:
updates.toolset !== undefined ? updates.toolset : existing.toolset,
};
// Clean up undefined/false values
if (!updated.pinned) delete updated.pinned;
if (!updated.memfs) delete updated.memfs;
if (!updated.toolset || updated.toolset === "auto")
delete updated.toolset;
if (!updated.baseUrl) delete updated.baseUrl;
agents[idx] = updated;
} else {
@@ -1333,6 +1346,8 @@ class SettingsManager {
// Clean up undefined/false values
if (!newAgent.pinned) delete newAgent.pinned;
if (!newAgent.memfs) delete newAgent.memfs;
if (!newAgent.toolset || newAgent.toolset === "auto")
delete newAgent.toolset;
if (!newAgent.baseUrl) delete newAgent.baseUrl;
agents.push(newAgent);
}
@@ -1354,6 +1369,40 @@ class SettingsManager {
this.upsertAgentSettings(agentId, { memfs: enabled });
}
/**
* Get toolset preference for an agent on the current server.
* Defaults to "auto" when no manual override is stored.
*/
getToolsetPreference(
agentId: string,
):
| "auto"
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none" {
return this.getAgentSettings(agentId)?.toolset ?? "auto";
}
/**
* Set toolset preference for an agent on the current server.
*/
setToolsetPreference(
agentId: string,
preference:
| "auto"
| "codex"
| "codex_snake"
| "default"
| "gemini"
| "gemini_snake"
| "none",
): void {
this.upsertAgentSettings(agentId, { toolset: preference });
}
/**
* Check if local .letta directory exists (indicates existing project)
*/

View File

@@ -977,3 +977,37 @@ describe("Settings Manager - Agents Array Migration", () => {
expect(settingsManager.isMemfsEnabled("agent-persist-test")).toBe(true);
});
});
describe("Settings Manager - Toolset Preferences", () => {
test("getToolsetPreference defaults to auto", async () => {
await settingsManager.initialize();
expect(settingsManager.getToolsetPreference("agent-unset")).toBe("auto");
});
test("setToolsetPreference stores and clears manual override", async () => {
await settingsManager.initialize();
settingsManager.setToolsetPreference("agent-toolset", "codex");
expect(settingsManager.getToolsetPreference("agent-toolset")).toBe("codex");
settingsManager.setToolsetPreference("agent-toolset", "auto");
expect(settingsManager.getToolsetPreference("agent-toolset")).toBe("auto");
});
test("setToolsetPreference persists to disk", async () => {
await settingsManager.initialize();
settingsManager.setToolsetPreference("agent-toolset-persist", "gemini");
// Wait for async persist
await new Promise((resolve) => setTimeout(resolve, 100));
await settingsManager.reset();
await settingsManager.initialize();
expect(settingsManager.getToolsetPreference("agent-toolset-persist")).toBe(
"gemini",
);
});
});

View File

@@ -140,4 +140,15 @@ describe("Startup Flow - Smoke", () => {
expect(result.stderr).toContain("Missing LETTA_API_KEY");
expect(result.stderr).not.toContain("No recent session found");
});
test("--toolset auto is accepted", async () => {
const result = await runCli(
["--new-agent", "--toolset", "auto", "-p", "Say OK"],
{
expectExit: 1,
},
);
expect(result.stderr).toContain("Missing LETTA_API_KEY");
expect(result.stderr).not.toContain("Invalid toolset");
});
});

View File

@@ -5,6 +5,7 @@ import {
clearToolsWithLock,
GEMINI_PASCAL_TOOLS,
getToolNames,
isGeminiModel,
isOpenAIModel,
loadSpecificTools,
loadTools,
@@ -35,6 +36,18 @@ export type ToolsetName =
| "gemini"
| "gemini_snake"
| "none";
export type ToolsetPreference = ToolsetName | "auto";
export function deriveToolsetFromModel(
modelIdentifier: string,
): "codex" | "gemini" | "default" {
const resolvedModel = resolveModel(modelIdentifier) ?? modelIdentifier;
return isOpenAIModel(resolvedModel)
? "codex"
: isGeminiModel(resolvedModel)
? "gemini"
: "default";
}
/**
* Ensures the correct memory tool is attached to the agent based on the model.
@@ -248,11 +261,6 @@ export async function forceToolsetSwitch(
const useMemoryPatch =
toolsetName === "codex" || toolsetName === "codex_snake";
await ensureCorrectMemoryTool(agentId, modelForLoading, useMemoryPatch);
// NOTE: Toolset is not persisted. On resume, we derive from agent's model.
// If we want to persist explicit toolset overrides in the future, add:
// agentToolsets: Record<string, ToolsetName> to Settings (global, since agent IDs are UUIDs)
// and save here: settingsManager.updateSettings({ agentToolsets: { ...current, [agentId]: toolsetName } })
}
/**
@@ -293,13 +301,6 @@ export async function switchToolsetForModel(
// Ensure base memory tool is correct for the model
await ensureCorrectMemoryTool(agentId, resolvedModel);
const { isGeminiModel } = await import("./manager");
const toolsetName = isOpenAIModel(resolvedModel)
? "codex"
: isGeminiModel(resolvedModel)
? "gemini"
: "default";
// NOTE: Toolset is derived from model, not persisted. See comment in forceToolsetSwitch.
const toolsetName = deriveToolsetFromModel(resolvedModel);
return toolsetName;
}