Files
letta-code/src/tests/agent/subagent-model-resolution.test.ts
Sarah Wooders c2a1312811 feat: default agents and subagents to auto model (#1392)
Co-authored-by: Letta Code <noreply@letta.com>
Co-authored-by: Ari Webb <ari@letta.com>
2026-03-16 16:50:01 -07:00

303 lines
9.5 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import type { SubagentConfig } from "../../agent/subagents";
import {
buildSubagentArgs,
resolveSubagentLauncher,
resolveSubagentModel,
} from "../../agent/subagents/manager";
describe("resolveSubagentLauncher", () => {
test("explicit launcher takes precedence over .ts script autodetection", () => {
const launcher = resolveSubagentLauncher(["-p", "hi"], {
env: {
LETTA_CODE_BIN: "custom-bun",
LETTA_CODE_BIN_ARGS_JSON: JSON.stringify(["run", "src/index.ts"]),
} as NodeJS.ProcessEnv,
argv: ["bun", "/tmp/dev-entry.ts"],
execPath: "/opt/homebrew/bin/bun",
platform: "darwin",
});
expect(launcher).toEqual({
command: "custom-bun",
args: ["run", "src/index.ts", "-p", "hi"],
});
});
test("explicit launcher takes precedence over .js script autodetection", () => {
const launcher = resolveSubagentLauncher(["-p", "hi"], {
env: {
LETTA_CODE_BIN: "custom-node",
} as NodeJS.ProcessEnv,
argv: ["node", "/tmp/letta.js"],
execPath: "/usr/local/bin/node",
platform: "win32",
});
expect(launcher).toEqual({
command: "custom-node",
args: ["-p", "hi"],
});
});
test("preserves existing .ts dev behavior for any ts entrypoint", () => {
const launcher = resolveSubagentLauncher(
["--output-format", "stream-json"],
{
env: {} as NodeJS.ProcessEnv,
argv: ["bun", "/tmp/custom-runner.ts"],
execPath: "/opt/homebrew/bin/bun",
platform: "darwin",
},
);
expect(launcher).toEqual({
command: "/opt/homebrew/bin/bun",
args: ["/tmp/custom-runner.ts", "--output-format", "stream-json"],
});
});
test("uses node runtime for bundled js on win32", () => {
const launcher = resolveSubagentLauncher(["-p", "prompt"], {
env: {} as NodeJS.ProcessEnv,
argv: ["node", "C:\\Program Files\\Letta\\letta.js"],
execPath: "C:\\Program Files\\nodejs\\node.exe",
platform: "win32",
});
expect(launcher).toEqual({
command: "C:\\Program Files\\nodejs\\node.exe",
args: ["C:\\Program Files\\Letta\\letta.js", "-p", "prompt"],
});
});
test("keeps direct js spawn behavior on non-win32", () => {
const launcher = resolveSubagentLauncher(["-p", "prompt"], {
env: {} as NodeJS.ProcessEnv,
argv: ["node", "/usr/local/lib/letta.js"],
execPath: "/usr/local/bin/node",
platform: "linux",
});
expect(launcher).toEqual({
command: "/usr/local/lib/letta.js",
args: ["-p", "prompt"],
});
});
test("falls back to global letta when no launcher hints available", () => {
const launcher = resolveSubagentLauncher(["-p", "prompt"], {
env: {} as NodeJS.ProcessEnv,
argv: ["node", ""],
execPath: "/usr/local/bin/node",
platform: "linux",
});
expect(launcher).toEqual({
command: "letta",
args: ["-p", "prompt"],
});
});
test("keeps explicit launcher with spaces as a single command token", () => {
const launcher = resolveSubagentLauncher(
["--output-format", "stream-json"],
{
env: {
LETTA_CODE_BIN:
'"C:\\Users\\Example User\\AppData\\Roaming\\npm\\letta.cmd"',
} as NodeJS.ProcessEnv,
argv: ["node", "C:\\Program Files\\Letta\\letta.js"],
execPath: "C:\\Program Files\\nodejs\\node.exe",
platform: "win32",
},
);
expect(launcher).toEqual({
command: "C:\\Users\\Example User\\AppData\\Roaming\\npm\\letta.cmd",
args: ["--output-format", "stream-json"],
});
});
});
describe("buildSubagentArgs", () => {
const baseConfig: SubagentConfig = {
name: "test-subagent",
description: "test",
systemPrompt: "test prompt",
allowedTools: "all",
recommendedModel: "inherit",
skills: [],
memoryBlocks: "none",
mode: "stateful",
};
test("adds --no-memfs for newly spawned subagents by default", () => {
const args = buildSubagentArgs("test-subagent", baseConfig, null, "hello");
expect(args).toContain("--init-blocks");
expect(args).toContain("none");
expect(args).toContain("--no-memfs");
});
test("does not force --no-memfs when deploying an existing subagent agent", () => {
const args = buildSubagentArgs(
"test-subagent",
baseConfig,
null,
"hello",
"agent-existing",
);
expect(args).toContain("--agent");
expect(args).not.toContain("--new-agent");
expect(args).not.toContain("--no-memfs");
});
});
describe("resolveSubagentModel", () => {
test("prefers BYOK-swapped handle when available", async () => {
const cases = [
{ parentProvider: "lc-anthropic", baseProvider: "anthropic" },
{ parentProvider: "lc-openai", baseProvider: "openai" },
{ parentProvider: "lc-zai", baseProvider: "zai" },
{ parentProvider: "lc-gemini", baseProvider: "google_ai" },
{ parentProvider: "lc-openrouter", baseProvider: "openrouter" },
{ parentProvider: "lc-minimax", baseProvider: "minimax" },
{ parentProvider: "lc-bedrock", baseProvider: "bedrock" },
{ parentProvider: "chatgpt-plus-pro", baseProvider: "chatgpt-plus-pro" },
];
for (const { parentProvider, baseProvider } of cases) {
const recommendedHandle = `${baseProvider}/test-model`;
const swappedHandle = `${parentProvider}/test-model`;
const parentHandle = `${parentProvider}/parent-model`;
const result = await resolveSubagentModel({
recommendedModel: recommendedHandle,
parentModelHandle: parentHandle,
availableHandles: new Set([recommendedHandle, swappedHandle]),
});
expect(result).toBe(swappedHandle);
}
});
test("falls back to parent model when recommended is unavailable", async () => {
const result = await resolveSubagentModel({
recommendedModel: "anthropic/test-model",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(),
});
expect(result).toBe("lc-anthropic/parent-model");
});
test("BYOK parent ignores base-provider recommended when swap is unavailable", async () => {
const result = await resolveSubagentModel({
recommendedModel: "anthropic/test-model",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(["anthropic/test-model"]),
});
expect(result).toBe("lc-anthropic/parent-model");
});
test("BYOK parent accepts recommended handle when already using same BYOK prefix", async () => {
const result = await resolveSubagentModel({
recommendedModel: "lc-anthropic/test-model",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(["lc-anthropic/test-model"]),
});
expect(result).toBe("lc-anthropic/test-model");
});
test("uses recommended model when parent is not BYOK and model is available", async () => {
const result = await resolveSubagentModel({
recommendedModel: "anthropic/test-model",
parentModelHandle: "anthropic/parent-model",
availableHandles: new Set(["anthropic/test-model"]),
});
expect(result).toBe("anthropic/test-model");
});
test("explicit user model overrides all other resolution", async () => {
const result = await resolveSubagentModel({
userModel: "lc-openrouter/custom-model",
recommendedModel: "anthropic/test-model",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(["lc-anthropic/test-model"]),
});
expect(result).toBe("lc-openrouter/custom-model");
});
test("inherits parent when recommended is inherit", async () => {
const result = await resolveSubagentModel({
recommendedModel: "inherit",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(["lc-anthropic/parent-model"]),
});
expect(result).toBe("lc-anthropic/parent-model");
});
test("uses auto default when available", async () => {
const result = await resolveSubagentModel({
recommendedModel: "sonnet-4.5",
availableHandles: new Set(["letta/auto", "anthropic/test-model"]),
});
expect(result).toBe("letta/auto");
});
test("uses auto-fast default for free tier when available", async () => {
const result = await resolveSubagentModel({
billingTier: "free",
availableHandles: new Set(["letta/auto-fast", "letta/auto"]),
});
expect(result).toBe("letta/auto-fast");
});
test("free tier falls back to auto when auto-fast is unavailable", async () => {
const result = await resolveSubagentModel({
billingTier: "free",
availableHandles: new Set(["letta/auto"]),
});
expect(result).toBe("letta/auto");
});
test("falls back when auto is unavailable", async () => {
const result = await resolveSubagentModel({
recommendedModel: "anthropic/test-model",
availableHandles: new Set(["anthropic/test-model"]),
});
expect(result).toBe("anthropic/test-model");
});
test("keeps inherit behavior when auto is unavailable", async () => {
const result = await resolveSubagentModel({
recommendedModel: "inherit",
parentModelHandle: "openai/gpt-5",
availableHandles: new Set(["openai/gpt-5"]),
});
expect(result).toBe("openai/gpt-5");
});
test("user-provided model still overrides default auto", async () => {
const result = await resolveSubagentModel({
userModel: "openai/gpt-5",
recommendedModel: "sonnet-4.5",
availableHandles: new Set(["letta/auto", "openai/gpt-5"]),
});
expect(result).toBe("openai/gpt-5");
});
});