fix(subagents): handle CRLF/BOM frontmatter for custom agent files (#1424)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Devansh Jain
2026-03-17 15:07:59 -07:00
committed by GitHub
parent b7600ee2f8
commit ef7defe5d9
3 changed files with 152 additions and 4 deletions

View File

@@ -1,5 +1,39 @@
import { describe, expect, test } from "bun:test";
import { getAllSubagentConfigs } from "../../agent/subagents";
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
clearSubagentConfigCache,
getAllSubagentConfigs,
} from "../../agent/subagents";
let tempDir: string | null = null;
function createTempProjectDir(): string {
return mkdtempSync(join(tmpdir(), "letta-subagents-test-"));
}
function writeCustomSubagent(
projectDir: string,
fileName: string,
content: string,
) {
const agentsDir = join(projectDir, ".letta", "agents");
mkdirSync(agentsDir, { recursive: true });
writeFileSync(join(agentsDir, fileName), content, "utf-8");
}
beforeEach(() => {
clearSubagentConfigCache();
});
afterEach(() => {
clearSubagentConfigCache();
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
tempDir = null;
}
});
describe("built-in subagents", () => {
test("includes reflection subagent in available configs", async () => {
@@ -15,4 +49,68 @@ describe("built-in subagents", () => {
expect(configs["general-purpose"]?.mode).toBe("stateful");
expect(configs.memory?.mode).toBe("stateful");
});
test("custom CRLF reflection override replaces built-in reflection", async () => {
tempDir = createTempProjectDir();
writeCustomSubagent(
tempDir,
"reflection.md",
[
"---",
"name: reflection",
"description: Custom reflection override",
"tools: Read",
"model: zaisigno/glm-5",
"memoryBlocks: none",
"---",
"Custom prompt body",
].join("\r\n"),
);
const configs = await getAllSubagentConfigs(tempDir);
expect(configs.reflection).toBeDefined();
expect(configs.reflection?.description).toBe("Custom reflection override");
expect(configs.reflection?.recommendedModel).toBe("zaisigno/glm-5");
});
test("blank model field falls back to inherit", async () => {
tempDir = createTempProjectDir();
writeCustomSubagent(
tempDir,
"reflection.md",
`---
name: reflection
description: Custom reflection override
tools: Read
model:
memoryBlocks: none
---
Custom prompt body`,
);
const configs = await getAllSubagentConfigs(tempDir);
expect(configs.reflection).toBeDefined();
expect(configs.reflection?.recommendedModel).toBe("inherit");
});
test("frontmatter name remains override key (filename can differ)", async () => {
tempDir = createTempProjectDir();
writeCustomSubagent(
tempDir,
"reflector.md",
`---
name: reflection
description: Custom reflection override from different filename
tools: Read
memoryBlocks: none
---
Custom prompt body`,
);
const configs = await getAllSubagentConfigs(tempDir);
expect(configs.reflection).toBeDefined();
expect(configs.reflection?.description).toBe(
"Custom reflection override from different filename",
);
});
});

View File

@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test";
import { parseFrontmatter } from "../../utils/frontmatter";
describe("parseFrontmatter", () => {
test("parses LF frontmatter", () => {
const content = `---
name: reflection
description: custom reflection
---
Prompt body`;
const { frontmatter, body } = parseFrontmatter(content);
expect(frontmatter.name).toBe("reflection");
expect(frontmatter.description).toBe("custom reflection");
expect(body).toBe("Prompt body");
});
test("parses CRLF frontmatter", () => {
const content =
"---\r\nname: reflection\r\ndescription: custom reflection\r\n---\r\nPrompt body\r\nLine 2";
const { frontmatter, body } = parseFrontmatter(content);
expect(frontmatter.name).toBe("reflection");
expect(frontmatter.description).toBe("custom reflection");
expect(body).toBe("Prompt body\nLine 2");
expect(body.includes("\r")).toBe(false);
});
test("parses BOM + CRLF frontmatter", () => {
const content =
"\uFEFF---\r\nname: reflection\r\ndescription: custom reflection\r\n---\r\nPrompt body";
const { frontmatter, body } = parseFrontmatter(content);
expect(frontmatter.name).toBe("reflection");
expect(frontmatter.description).toBe("custom reflection");
expect(body).toBe("Prompt body");
});
});

View File

@@ -31,11 +31,20 @@ export function parseFrontmatter(content: string): {
frontmatter: Record<string, string | string[]>;
body: string;
} {
// Normalize common cross-platform file encodings so frontmatter parsing
// works for user-authored files in .letta/agents/.
// - Strip UTF-8 BOM when present
// - Normalize CRLF (and lone CR) to LF
const normalized = content
.replace(/^\uFEFF/, "")
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
const match = normalized.match(frontmatterRegex);
if (!match || !match[1] || !match[2]) {
return { frontmatter: {}, body: content };
return { frontmatter: {}, body: normalized };
}
const frontmatterText = match[1];