fix: recover default agent creation when base tools are missing (#1039)

This commit is contained in:
Charles Packer
2026-02-19 12:47:03 -08:00
committed by GitHub
parent a496204409
commit 277ce3cd41
2 changed files with 195 additions and 4 deletions

View File

@@ -7,8 +7,10 @@ import type {
AgentType,
} from "@letta-ai/letta-client/resources/agents/agents";
import { DEFAULT_AGENT_NAME } from "../constants";
import { settingsManager } from "../settings-manager";
import { getModelContextWindow } from "./available-models";
import { getClient } from "./client";
import { getClient, getServerUrl } from "./client";
import { getLettaCodeHeaders } from "./http-headers";
import { getDefaultMemoryBlocks } from "./memory";
import { type MemoryPromptMode, reconcileMemoryPrompt } from "./memoryPrompt";
import {
@@ -45,6 +47,88 @@ export interface CreateAgentResult {
provenance: AgentProvenance;
}
function isToolsNotFoundError(err: unknown): boolean {
const message = err instanceof Error ? err.message : String(err);
const status = (err as { status?: unknown } | null)?.status;
return (
typeof message === "string" &&
/tools not found by name/i.test(message) &&
/memory_apply_patch|memory|web_search|fetch_webpage/i.test(message) &&
(status === undefined || status === 400)
);
}
async function addBaseToolsToServer(): Promise<boolean> {
const settings = await settingsManager.getSettingsWithSecureTokens();
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
if (!apiKey) {
console.warn(
"Cannot auto-populate base tools: missing LETTA_API_KEY for manual endpoint call.",
);
return false;
}
try {
const response = await fetch(`${getServerUrl()}/v1/tools/add-base-tools`, {
method: "POST",
headers: getLettaCodeHeaders(apiKey),
});
if (!response.ok) {
const body = await response.text();
console.warn(
`Failed to add base tools via /v1/tools/add-base-tools (${response.status}): ${body || response.statusText}`,
);
return false;
}
return true;
} catch (err) {
console.warn(
`Failed to call /v1/tools/add-base-tools: ${err instanceof Error ? err.message : String(err)}`,
);
return false;
}
}
type CreateWithToolsFn = (tools: string[]) => Promise<AgentState>;
type AddBaseToolsFn = () => Promise<boolean>;
export async function createAgentWithBaseToolsRecovery(
createWithTools: CreateWithToolsFn,
toolNames: string[],
addBaseTools: AddBaseToolsFn = addBaseToolsToServer,
): Promise<AgentState> {
try {
return await createWithTools(toolNames);
} catch (err) {
if (!isToolsNotFoundError(err)) {
throw err;
}
console.warn(
"Agent creation failed due to missing base tools. Attempting to add base tools on server...",
);
await addBaseTools();
try {
return await createWithTools(toolNames);
} catch (retryErr) {
console.warn(
`Agent creation still failed after base-tool bootstrap: ${
retryErr instanceof Error ? retryErr.message : String(retryErr)
}`,
);
console.warn(
"Retrying agent creation with no server-side tools attached.",
);
return await createWithTools([]);
}
}
}
export interface CreateAgentOptions {
name?: string;
/** Agent description shown in /agents selector */
@@ -306,7 +390,7 @@ export async function createAgent(
const agentDescription =
options.description ?? `Letta Code agent created in ${process.cwd()}`;
const agent = await client.agents.create({
const createAgentRequestBase = {
agent_type: "letta_v1_agent" as AgentType,
system: systemPromptContent,
name,
@@ -314,7 +398,6 @@ export async function createAgent(
embedding: embeddingModelVal || undefined,
model: modelHandle,
...(contextWindow && { context_window_limit: contextWindow }),
tools: toolNames,
// New blocks created inline with agent (saves ~2s of sequential API calls)
memory_blocks:
filteredMemoryBlocks.length > 0 ? filteredMemoryBlocks : undefined,
@@ -328,7 +411,19 @@ export async function createAgent(
initial_message_sequence: [],
parallel_tool_calls: parallelToolCallsVal,
enable_sleeptime: enableSleeptimeVal,
});
};
const createWithTools = (tools: string[]) =>
client.agents.create({
...createAgentRequestBase,
tools,
});
const agent = await createAgentWithBaseToolsRecovery(
createWithTools,
toolNames,
addBaseToolsToServer,
);
// Note: Preflight check above falls back to 'memory' when 'memory_apply_patch' is unavailable.

View File

@@ -0,0 +1,96 @@
import { describe, expect, mock, test } from "bun:test";
import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
import { createAgentWithBaseToolsRecovery } from "../../agent/create";
function missingBaseToolsError(): Error & { status: number } {
return Object.assign(
new Error(
`400 {"detail":"Tools not found by name: {'fetch_webpage', 'memory'}"}`,
),
{ status: 400 },
);
}
describe("createAgentWithBaseToolsRecovery", () => {
const mkAgent = (id: string): AgentState => ({ id }) as unknown as AgentState;
test("bootstraps base tools then retries with original tools", async () => {
const createWithTools = mock((_tools: string[]) => {
if (createWithTools.mock.calls.length === 1) {
return Promise.reject(missingBaseToolsError());
}
return Promise.resolve(mkAgent("agent-retry-success"));
});
const addBaseTools = mock(() => Promise.resolve(true));
const agent = await createAgentWithBaseToolsRecovery(
createWithTools,
["memory", "web_search", "fetch_webpage"],
addBaseTools,
);
expect(agent.id).toBe("agent-retry-success");
expect(addBaseTools).toHaveBeenCalledTimes(1);
expect(createWithTools).toHaveBeenCalledTimes(2);
expect(createWithTools.mock.calls[0]?.[0]).toEqual([
"memory",
"web_search",
"fetch_webpage",
]);
expect(createWithTools.mock.calls[1]?.[0]).toEqual([
"memory",
"web_search",
"fetch_webpage",
]);
});
test("falls back to create with no server-side tools after second failure", async () => {
const createWithTools = mock((_tools: string[]) => {
if (createWithTools.mock.calls.length <= 2) {
return Promise.reject(
createWithTools.mock.calls.length === 1
? missingBaseToolsError()
: new Error("still failing after bootstrap"),
);
}
return Promise.resolve(mkAgent("agent-no-tools"));
});
const addBaseTools = mock(() => Promise.resolve(true));
const agent = await createAgentWithBaseToolsRecovery(
createWithTools,
["memory", "web_search", "fetch_webpage"],
addBaseTools,
);
expect(agent.id).toBe("agent-no-tools");
expect(addBaseTools).toHaveBeenCalledTimes(1);
expect(createWithTools).toHaveBeenCalledTimes(3);
expect(createWithTools.mock.calls[2]?.[0]).toEqual([]);
});
test("does not bootstrap for unrelated missing-tool errors", async () => {
const createWithTools = mock(() =>
Promise.reject(
Object.assign(
new Error(
`400 {"detail":"Tools not found by name: {'custom_tool'}"}`,
),
{ status: 400 },
),
),
);
const addBaseTools = mock(() => Promise.resolve(true));
await expect(
createAgentWithBaseToolsRecovery(
createWithTools,
["custom_tool"],
addBaseTools,
),
).rejects.toThrow("custom_tool");
expect(addBaseTools).not.toHaveBeenCalled();
expect(createWithTools).toHaveBeenCalledTimes(1);
});
});