fix: recover default agent creation when base tools are missing (#1039)
This commit is contained in:
@@ -7,8 +7,10 @@ import type {
|
|||||||
AgentType,
|
AgentType,
|
||||||
} from "@letta-ai/letta-client/resources/agents/agents";
|
} from "@letta-ai/letta-client/resources/agents/agents";
|
||||||
import { DEFAULT_AGENT_NAME } from "../constants";
|
import { DEFAULT_AGENT_NAME } from "../constants";
|
||||||
|
import { settingsManager } from "../settings-manager";
|
||||||
import { getModelContextWindow } from "./available-models";
|
import { getModelContextWindow } from "./available-models";
|
||||||
import { getClient } from "./client";
|
import { getClient, getServerUrl } from "./client";
|
||||||
|
import { getLettaCodeHeaders } from "./http-headers";
|
||||||
import { getDefaultMemoryBlocks } from "./memory";
|
import { getDefaultMemoryBlocks } from "./memory";
|
||||||
import { type MemoryPromptMode, reconcileMemoryPrompt } from "./memoryPrompt";
|
import { type MemoryPromptMode, reconcileMemoryPrompt } from "./memoryPrompt";
|
||||||
import {
|
import {
|
||||||
@@ -45,6 +47,88 @@ export interface CreateAgentResult {
|
|||||||
provenance: AgentProvenance;
|
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 {
|
export interface CreateAgentOptions {
|
||||||
name?: string;
|
name?: string;
|
||||||
/** Agent description shown in /agents selector */
|
/** Agent description shown in /agents selector */
|
||||||
@@ -306,7 +390,7 @@ export async function createAgent(
|
|||||||
const agentDescription =
|
const agentDescription =
|
||||||
options.description ?? `Letta Code agent created in ${process.cwd()}`;
|
options.description ?? `Letta Code agent created in ${process.cwd()}`;
|
||||||
|
|
||||||
const agent = await client.agents.create({
|
const createAgentRequestBase = {
|
||||||
agent_type: "letta_v1_agent" as AgentType,
|
agent_type: "letta_v1_agent" as AgentType,
|
||||||
system: systemPromptContent,
|
system: systemPromptContent,
|
||||||
name,
|
name,
|
||||||
@@ -314,7 +398,6 @@ export async function createAgent(
|
|||||||
embedding: embeddingModelVal || undefined,
|
embedding: embeddingModelVal || undefined,
|
||||||
model: modelHandle,
|
model: modelHandle,
|
||||||
...(contextWindow && { context_window_limit: contextWindow }),
|
...(contextWindow && { context_window_limit: contextWindow }),
|
||||||
tools: toolNames,
|
|
||||||
// New blocks created inline with agent (saves ~2s of sequential API calls)
|
// New blocks created inline with agent (saves ~2s of sequential API calls)
|
||||||
memory_blocks:
|
memory_blocks:
|
||||||
filteredMemoryBlocks.length > 0 ? filteredMemoryBlocks : undefined,
|
filteredMemoryBlocks.length > 0 ? filteredMemoryBlocks : undefined,
|
||||||
@@ -328,7 +411,19 @@ export async function createAgent(
|
|||||||
initial_message_sequence: [],
|
initial_message_sequence: [],
|
||||||
parallel_tool_calls: parallelToolCallsVal,
|
parallel_tool_calls: parallelToolCallsVal,
|
||||||
enable_sleeptime: enableSleeptimeVal,
|
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.
|
// 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