fix: make toolset switching atomic to prevent tool desync race (#648)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-22 17:33:14 -08:00
committed by GitHub
parent ebe3a344f1
commit e32b10f931
3 changed files with 260 additions and 116 deletions

View File

@@ -2,7 +2,7 @@ import { getClient } from "../agent/client";
import { resolveModel } from "../agent/model";
import { toolFilter } from "./filter";
import {
clearTools,
clearToolsWithLock,
GEMINI_PASCAL_TOOLS,
getToolNames,
isOpenAIModel,
@@ -121,14 +121,14 @@ export async function forceToolsetSwitch(
toolsetName: ToolsetName,
agentId: string,
): Promise<void> {
// Clear currently loaded tools
clearTools();
// Load the appropriate toolset
// Map toolset name to a model identifier for loading
// Note: loadTools/loadSpecificTools acquire a switch lock that causes
// sendMessageStream to wait, preventing messages from being sent with
// stale or partial tools during the switch.
let modelForLoading: string;
if (toolsetName === "none") {
// Just clear tools, no loading needed
// Clear tools with lock protection so sendMessageStream() waits
clearToolsWithLock();
return;
} else if (toolsetName === "codex") {
await loadSpecificTools([...CODEX_TOOLS]);
@@ -174,8 +174,9 @@ export async function switchToolsetForModel(
// Resolve model ID to handle when possible so provider checks stay consistent
const resolvedModel = resolveModel(modelIdentifier) ?? modelIdentifier;
// Clear currently loaded tools and load the appropriate set for the target model
clearTools();
// Load the appropriate set for the target model
// Note: loadTools acquires a switch lock that causes sendMessageStream to wait,
// preventing messages from being sent with stale or partial tools during the switch.
await loadTools(resolvedModel);
// If no tools were loaded (e.g., unexpected handle or edge-case filter),