feat: new flag to filter blocks on creation (#132)

This commit is contained in:
Charles Packer
2025-11-27 02:12:41 -08:00
committed by GitHub
parent 135c19c7d7
commit 06ec8b9d3c
4 changed files with 251 additions and 5 deletions

View File

@@ -32,6 +32,8 @@ export async function createAgent(
parallelToolCalls = true,
enableSleeptime = false,
systemPromptId?: string,
initBlocks?: string[],
baseTools?: string[],
) {
// Resolve model identifier to handle
let modelHandle: string;
@@ -59,16 +61,42 @@ export async function createAgent(
getServerToolName(name),
);
const toolNames = [
...serverToolNames,
const defaultBaseTools = baseTools ?? [
"memory",
"web_search",
"conversation_search",
"fetch_webpage",
];
const toolNames = [...serverToolNames, ...defaultBaseTools];
// Load memory blocks from .mdx files
const defaultMemoryBlocks = await getDefaultMemoryBlocks();
const defaultMemoryBlocks =
initBlocks && initBlocks.length === 0 ? [] : await getDefaultMemoryBlocks();
// Optional filter: only initialize a subset of memory blocks on creation
const allowedBlockLabels = initBlocks
? new Set(
initBlocks.map((name) => name.trim()).filter((name) => name.length > 0),
)
: undefined;
if (allowedBlockLabels && allowedBlockLabels.size > 0) {
const knownLabels = new Set(defaultMemoryBlocks.map((b) => b.label));
for (const label of Array.from(allowedBlockLabels)) {
if (!knownLabels.has(label)) {
console.warn(
`Ignoring unknown init block "${label}". Valid blocks: ${Array.from(knownLabels).join(", ")}`,
);
allowedBlockLabels.delete(label);
}
}
}
const filteredMemoryBlocks =
allowedBlockLabels && allowedBlockLabels.size > 0
? defaultMemoryBlocks.filter((b) => allowedBlockLabels.has(b.label))
: defaultMemoryBlocks;
// Resolve absolute path for skills directory
const resolvedSkillsDirectory =
@@ -87,7 +115,7 @@ export async function createAgent(
}
// Find and update the skills memory block with discovered skills
const skillsBlock = defaultMemoryBlocks.find((b) => b.label === "skills");
const skillsBlock = filteredMemoryBlocks.find((b) => b.label === "skills");
if (skillsBlock) {
skillsBlock.value = formatSkillsForMemory(
skills,
@@ -116,6 +144,9 @@ export async function createAgent(
if (!forceNewBlocks) {
// Load global blocks (persona, human)
for (const [label, blockId] of Object.entries(globalSharedBlockIds)) {
if (allowedBlockLabels && !allowedBlockLabels.has(label)) {
continue;
}
try {
const block = await client.blocks.retrieve(blockId);
existingBlocks.set(label, block);
@@ -129,6 +160,9 @@ export async function createAgent(
// Load local blocks (style)
for (const [label, blockId] of Object.entries(localSharedBlockIds)) {
if (allowedBlockLabels && !allowedBlockLabels.has(label)) {
continue;
}
try {
const block = await client.blocks.retrieve(blockId);
existingBlocks.set(label, block);
@@ -145,7 +179,7 @@ export async function createAgent(
const blockIds: string[] = [];
const blocksToCreate: Array<{ block: CreateBlock; label: string }> = [];
for (const defaultBlock of defaultMemoryBlocks) {
for (const defaultBlock of filteredMemoryBlocks) {
const existingBlock = existingBlocks.get(defaultBlock.label);
if (existingBlock?.id) {
// Reuse existing global shared block

View File

@@ -52,6 +52,8 @@ export async function handleHeadlessCommand(
link: { type: "boolean" },
unlink: { type: "boolean" },
sleeptime: { type: "boolean" },
"init-blocks": { type: "string" },
"base-tools": { type: "string" },
},
strict: false,
allowPositionals: true,
@@ -84,8 +86,50 @@ export async function handleHeadlessCommand(
const specifiedAgentId = values.agent as string | undefined;
const shouldContinue = values.continue as boolean | undefined;
const forceNew = values.new as boolean | undefined;
const initBlocksRaw = values["init-blocks"] as string | undefined;
const baseToolsRaw = values["base-tools"] as string | undefined;
const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined;
if (initBlocksRaw && !forceNew) {
console.error(
"Error: --init-blocks can only be used together with --new to control initial memory blocks.",
);
process.exit(1);
}
let initBlocks: string[] | undefined;
if (initBlocksRaw !== undefined) {
const trimmed = initBlocksRaw.trim();
if (!trimmed || trimmed.toLowerCase() === "none") {
initBlocks = [];
} else {
initBlocks = trimmed
.split(",")
.map((name) => name.trim())
.filter((name) => name.length > 0);
}
}
if (baseToolsRaw && !forceNew) {
console.error(
"Error: --base-tools can only be used together with --new to control initial base tools.",
);
process.exit(1);
}
let baseTools: string[] | undefined;
if (baseToolsRaw !== undefined) {
const trimmed = baseToolsRaw.trim();
if (!trimmed || trimmed.toLowerCase() === "none") {
baseTools = [];
} else {
baseTools = trimmed
.split(",")
.map((name) => name.trim())
.filter((name) => name.length > 0);
}
}
// Priority 1: Try to use --agent specified ID
if (specifiedAgentId) {
try {
@@ -107,6 +151,9 @@ export async function handleHeadlessCommand(
skillsDirectory,
settings.parallelToolCalls,
sleeptimeFlag ?? settings.enableSleeptime,
undefined,
initBlocks,
baseTools,
);
}
@@ -148,6 +195,9 @@ export async function handleHeadlessCommand(
skillsDirectory,
settings.parallelToolCalls,
sleeptimeFlag ?? settings.enableSleeptime,
undefined,
undefined,
undefined,
);
}

View File

@@ -29,6 +29,8 @@ OPTIONS
-v, --version Print version and exit
--new Create new agent (reuses global blocks like persona/human)
--fresh-blocks Force create all new memory blocks (isolate from other agents)
--init-blocks <list> Comma-separated memory blocks to initialize when using --new (e.g., "persona,skills")
--base-tools <list> Comma-separated base tools to attach when using --new (e.g., "memory,web_search,conversation_search")
-c, --continue Resume previous session (uses global lastAgent, deprecated)
-a, --agent <id> Use a specific agent ID
-m, --model <id> Model ID or handle (e.g., "opus-4.5" or "anthropic/claude-opus-4-5")
@@ -121,6 +123,8 @@ async function main() {
continue: { type: "boolean", short: "c" },
new: { type: "boolean" },
"fresh-blocks": { type: "boolean" },
"init-blocks": { type: "string" },
"base-tools": { type: "string" },
agent: { type: "string", short: "a" },
model: { type: "string", short: "m" },
system: { type: "string", short: "s" },
@@ -177,6 +181,8 @@ async function main() {
const shouldContinue = (values.continue as boolean | undefined) ?? false;
const forceNew = (values.new as boolean | undefined) ?? false;
const freshBlocks = (values["fresh-blocks"] as boolean | undefined) ?? false;
const initBlocksRaw = values["init-blocks"] as string | undefined;
const baseToolsRaw = values["base-tools"] as string | undefined;
const specifiedAgentId = (values.agent as string | undefined) ?? null;
const specifiedModel = (values.model as string | undefined) ?? undefined;
const specifiedSystem = (values.system as string | undefined) ?? undefined;
@@ -185,6 +191,49 @@ async function main() {
const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined;
const isHeadless = values.prompt || values.run || !process.stdin.isTTY;
// --init-blocks only makes sense when creating a brand new agent
if (initBlocksRaw && !forceNew) {
console.error(
"Error: --init-blocks can only be used together with --new to control initial memory blocks.",
);
process.exit(1);
}
let initBlocks: string[] | undefined;
if (initBlocksRaw !== undefined) {
const trimmed = initBlocksRaw.trim();
if (!trimmed || trimmed.toLowerCase() === "none") {
// Explicitly requested zero blocks
initBlocks = [];
} else {
initBlocks = trimmed
.split(",")
.map((name) => name.trim())
.filter((name) => name.length > 0);
}
}
// --base-tools only makes sense when creating a brand new agent
if (baseToolsRaw && !forceNew) {
console.error(
"Error: --base-tools can only be used together with --new to control initial base tools.",
);
process.exit(1);
}
let baseTools: string[] | undefined;
if (baseToolsRaw !== undefined) {
const trimmed = baseToolsRaw.trim();
if (!trimmed || trimmed.toLowerCase() === "none") {
baseTools = [];
} else {
baseTools = trimmed
.split(",")
.map((name) => name.trim())
.filter((name) => name.length > 0);
}
}
// Validate toolset if provided
if (
specifiedToolset &&
@@ -350,6 +399,8 @@ async function main() {
continueSession,
forceNew,
freshBlocks,
initBlocks,
baseTools,
agentIdArg,
model,
system,
@@ -359,6 +410,8 @@ async function main() {
continueSession: boolean;
forceNew: boolean;
freshBlocks: boolean;
initBlocks?: string[];
baseTools?: string[];
agentIdArg: string | null;
model?: string;
system?: string;
@@ -514,6 +567,8 @@ async function main() {
settings.parallelToolCalls,
sleeptimeFlag ?? settings.enableSleeptime,
system,
initBlocks,
baseTools,
);
}
@@ -561,6 +616,8 @@ async function main() {
settings.parallelToolCalls,
sleeptimeFlag ?? settings.enableSleeptime,
system,
undefined,
undefined,
);
}
@@ -633,6 +690,8 @@ async function main() {
continueSession: shouldContinue,
forceNew: forceNew,
freshBlocks: freshBlocks,
initBlocks: initBlocks,
baseTools: baseTools,
agentIdArg: specifiedAgentId,
model: specifiedModel,
system: specifiedSystem,

View File

@@ -0,0 +1,103 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { getClient } from "../../agent/client";
import { createAgent } from "../../agent/create";
import { settingsManager } from "../../settings-manager";
// Skip these integration tests if LETTA_API_KEY is not set
const shouldSkip = !process.env.LETTA_API_KEY;
const describeOrSkip = shouldSkip ? describe.skip : describe;
describeOrSkip("createAgent init-blocks filtering", () => {
let originalGlobalSharedBlockIds: Record<string, string>;
let originalLocalSharedBlockIds: Record<string, string>;
let createdAgentId: string | null = null;
beforeAll(async () => {
const apiKey = process.env.LETTA_API_KEY;
if (!apiKey) {
throw new Error("LETTA_API_KEY must be set to run this test");
}
await settingsManager.initialize();
const settings = settingsManager.getSettings();
await settingsManager.loadProjectSettings();
const projectSettings = settingsManager.getProjectSettings();
originalGlobalSharedBlockIds = { ...settings.globalSharedBlockIds };
originalLocalSharedBlockIds = { ...projectSettings.localSharedBlockIds };
});
afterAll(async () => {
const client = await getClient();
if (createdAgentId) {
try {
await client.agents.delete(createdAgentId);
} catch {
// Ignore cleanup errors
}
}
// Restore original shared block mappings to avoid polluting user settings
settingsManager.updateSettings({
globalSharedBlockIds: originalGlobalSharedBlockIds,
});
settingsManager.updateProjectSettings(
{
localSharedBlockIds: originalLocalSharedBlockIds,
},
process.cwd(),
);
});
test(
"only requested memory blocks are created/registered",
async () => {
const agent = await createAgent(
"init-blocks-test",
undefined,
"openai/text-embedding-3-small",
undefined,
true, // force new blocks instead of reusing shared ones
undefined,
true,
false,
undefined,
["persona", "skills"],
undefined,
);
createdAgentId = agent.id;
const settings = settingsManager.getSettings();
await settingsManager.loadProjectSettings();
const projectSettings = settingsManager.getProjectSettings();
const globalIds = settings.globalSharedBlockIds;
const localIds = projectSettings.localSharedBlockIds;
// Requested blocks must be present
expect(globalIds.persona).toBeDefined();
expect(localIds.skills).toBeDefined();
// No new GLOBAL shared blocks outside of the allowed set
const newGlobalLabels = Object.keys(globalIds).filter(
(label) => !(label in originalGlobalSharedBlockIds),
);
const disallowedGlobalLabels = newGlobalLabels.filter(
(label) => label !== "persona",
);
expect(disallowedGlobalLabels.length).toBe(0);
// No new LOCAL shared blocks outside of the allowed set
const newLocalLabels = Object.keys(localIds).filter(
(label) => !(label in originalLocalSharedBlockIds),
);
const disallowedLocalLabels = newLocalLabels.filter(
(label) => label !== "skills",
);
expect(disallowedLocalLabels.length).toBe(0);
},
{ timeout: 90000 },
);
});