fix: recover default agent creation when base tools are missing (#1039)
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
96
src/tests/agent/create-base-tools-recovery.test.ts
Normal file
96
src/tests/agent/create-base-tools-recovery.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user