feat: new flag to filter blocks on creation (#132)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
59
src/index.ts
59
src/index.ts
@@ -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,
|
||||
|
||||
103
src/tests/agent/init-blocks.test.ts
Normal file
103
src/tests/agent/init-blocks.test.ts
Normal 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 },
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user