feat: add skill source controls and headless reminder settings (#989)

This commit is contained in:
Charles Packer
2026-02-16 23:01:03 -08:00
committed by GitHub
parent 4125c12dc1
commit 6a2b2f6346
9 changed files with 459 additions and 34 deletions

View File

@@ -3,10 +3,13 @@
* This allows tools to access the current agent ID without threading it through params.
*/
import { ALL_SKILL_SOURCES } from "./skillSources";
import type { SkillSource } from "./skills";
interface AgentContext {
agentId: string | null;
skillsDirectory: string | null;
noSkills: boolean;
skillSources: SkillSource[];
conversationId: string | null;
}
@@ -24,7 +27,7 @@ function getContext(): AgentContext {
global[CONTEXT_KEY] = {
agentId: null,
skillsDirectory: null,
noSkills: false,
skillSources: [...ALL_SKILL_SOURCES],
conversationId: null,
};
}
@@ -37,16 +40,17 @@ const context = getContext();
* Set the current agent context
* @param agentId - The agent ID
* @param skillsDirectory - Optional skills directory path
* @param noSkills - Whether to skip bundled skills
* @param skillSources - Enabled skill sources for this session
*/
export function setAgentContext(
agentId: string,
skillsDirectory?: string,
noSkills?: boolean,
skillSources?: SkillSource[],
): void {
context.agentId = agentId;
context.skillsDirectory = skillsDirectory || null;
context.noSkills = noSkills ?? false;
context.skillSources =
skillSources !== undefined ? [...skillSources] : [...ALL_SKILL_SOURCES];
}
/**
@@ -76,10 +80,17 @@ export function getSkillsDirectory(): string | null {
}
/**
* Get whether bundled skills should be skipped
* Get enabled skill sources for discovery/injection.
*/
export function getSkillSources(): SkillSource[] {
return [...context.skillSources];
}
/**
* Backwards-compat helper: returns true when bundled skills are disabled.
*/
export function getNoSkills(): boolean {
return context.noSkills;
return !context.skillSources.includes("bundled");
}
/**

82
src/agent/skillSources.ts Normal file
View File

@@ -0,0 +1,82 @@
import type { SkillSource } from "./skills";
export const ALL_SKILL_SOURCES: SkillSource[] = [
"bundled",
"global",
"agent",
"project",
];
export type SkillSourceSpecifier = SkillSource | "all";
export type SkillSourceSelectionInput = {
skillSourcesRaw?: string;
noSkills?: boolean;
noBundledSkills?: boolean;
};
const VALID_SKILL_SOURCE_SPECIFIERS: SkillSourceSpecifier[] = [
"all",
...ALL_SKILL_SOURCES,
];
function isSkillSource(value: string): value is SkillSource {
return ALL_SKILL_SOURCES.includes(value as SkillSource);
}
function normalizeSkillSources(sources: SkillSource[]): SkillSource[] {
const sourceSet = new Set(sources);
return ALL_SKILL_SOURCES.filter((source) => sourceSet.has(source));
}
export function parseSkillSourcesList(skillSourcesRaw: string): SkillSource[] {
const tokens = skillSourcesRaw
.split(",")
.map((source) => source.trim())
.filter((source) => source.length > 0);
if (tokens.length === 0) {
throw new Error(
"--skill-sources must include at least one source (e.g. bundled,project)",
);
}
const sources: SkillSource[] = [];
for (const token of tokens) {
const source = token as SkillSourceSpecifier;
if (!VALID_SKILL_SOURCE_SPECIFIERS.includes(source)) {
throw new Error(
`Invalid skill source "${token}". Valid values: ${VALID_SKILL_SOURCE_SPECIFIERS.join(", ")}`,
);
}
if (source === "all") {
sources.push(...ALL_SKILL_SOURCES);
continue;
}
if (isSkillSource(source)) {
sources.push(source);
}
}
return normalizeSkillSources(sources);
}
export function resolveSkillSourcesSelection(
input: SkillSourceSelectionInput,
): SkillSource[] {
if (input.noSkills) {
return [];
}
const configuredSources = input.skillSourcesRaw
? parseSkillSourcesList(input.skillSourcesRaw)
: [...ALL_SKILL_SOURCES];
const filteredSources = input.noBundledSkills
? configuredSources.filter((source) => source !== "bundled")
: configuredSources;
return normalizeSkillSources(filteredSources);
}

View File

@@ -13,6 +13,7 @@ import { readdir, readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { parseFrontmatter } from "../utils/frontmatter";
import { ALL_SKILL_SOURCES } from "./skillSources";
/**
* Get the bundled skills directory path
@@ -69,6 +70,11 @@ export interface SkillDiscoveryResult {
errors: SkillDiscoveryError[];
}
export interface SkillDiscoveryOptions {
skipBundled?: boolean;
sources?: SkillSource[];
}
/**
* Represents an error that occurred during skill discovery
*/
@@ -167,13 +173,15 @@ async function discoverSkillsFromDir(
export async function discoverSkills(
projectSkillsPath: string = join(process.cwd(), SKILLS_DIR),
agentId?: string,
options?: { skipBundled?: boolean },
options?: SkillDiscoveryOptions,
): Promise<SkillDiscoveryResult> {
const allErrors: SkillDiscoveryError[] = [];
const skillsById = new Map<string, Skill>();
const sourceSet = new Set(options?.sources ?? ALL_SKILL_SOURCES);
const includeSource = (source: SkillSource) => sourceSet.has(source);
// 1. Start with bundled skills (lowest priority)
if (!options?.skipBundled) {
if (includeSource("bundled") && !options?.skipBundled) {
const bundledSkills = await getBundledSkills();
for (const skill of bundledSkills) {
skillsById.set(skill.id, skill);
@@ -181,14 +189,19 @@ export async function discoverSkills(
}
// 2. Add global skills (override bundled)
const globalResult = await discoverSkillsFromDir(GLOBAL_SKILLS_DIR, "global");
allErrors.push(...globalResult.errors);
for (const skill of globalResult.skills) {
skillsById.set(skill.id, skill);
if (includeSource("global")) {
const globalResult = await discoverSkillsFromDir(
GLOBAL_SKILLS_DIR,
"global",
);
allErrors.push(...globalResult.errors);
for (const skill of globalResult.skills) {
skillsById.set(skill.id, skill);
}
}
// 3. Add agent skills if agentId provided (override global)
if (agentId) {
if (agentId && includeSource("agent")) {
const agentSkillsDir = getAgentSkillsDir(agentId);
const agentResult = await discoverSkillsFromDir(agentSkillsDir, "agent");
allErrors.push(...agentResult.errors);
@@ -198,13 +211,15 @@ export async function discoverSkills(
}
// 4. Add project skills (override all - highest priority)
const projectResult = await discoverSkillsFromDir(
projectSkillsPath,
"project",
);
allErrors.push(...projectResult.errors);
for (const skill of projectResult.skills) {
skillsById.set(skill.id, skill);
if (includeSource("project")) {
const projectResult = await discoverSkillsFromDir(
projectSkillsPath,
"project",
);
allErrors.push(...projectResult.errors);
for (const skill of projectResult.skills) {
skillsById.set(skill.id, skill);
}
}
return {

View File

@@ -806,6 +806,7 @@ export default function App({
showCompactions = false,
agentProvenance = null,
releaseNotes = null,
sessionContextReminderEnabled = true,
}: {
agentId: string;
agentState?: AgentState | null;
@@ -825,6 +826,7 @@ export default function App({
showCompactions?: boolean;
agentProvenance?: AgentProvenance | null;
releaseNotes?: string | null; // Markdown release notes to display above header
sessionContextReminderEnabled?: boolean;
}) {
// Warm the model-access cache in the background so /model is fast on first open.
useEffect(() => {
@@ -8016,7 +8018,11 @@ ${SYSTEM_REMINDER_CLOSE}`;
const sessionContextEnabled = settingsManager.getSetting(
"sessionContextEnabled",
);
if (!hasSentSessionContextRef.current && sessionContextEnabled) {
if (
!hasSentSessionContextRef.current &&
sessionContextEnabled &&
sessionContextReminderEnabled
) {
const { buildSessionContext } = await import(
"./helpers/sessionContext"
);
@@ -8168,7 +8174,7 @@ ${SYSTEM_REMINDER_CLOSE}
SKILLS_DIR: defaultDir,
formatSkillsAsSystemReminder,
} = await import("../agent/skills");
const { getSkillsDirectory, getNoSkills } = await import(
const { getSkillsDirectory, getSkillSources } = await import(
"../agent/context"
);
@@ -8181,7 +8187,7 @@ ${SYSTEM_REMINDER_CLOSE}
const skillsDir =
getSkillsDirectory() || join(process.cwd(), defaultDir);
const { skills } = await discover(skillsDir, agentId, {
skipBundled: getNoSkills(),
sources: getSkillSources(),
});
latestSkills = skills;
} catch {
@@ -8895,6 +8901,7 @@ ${SYSTEM_REMINDER_CLOSE}
pendingRalphConfig,
openTrajectorySegment,
resetTrajectoryBases,
sessionContextReminderEnabled,
appendTaskNotificationEvents,
],
);

View File

@@ -52,14 +52,14 @@ export function SkillsDialog({ onClose, agentId }: SkillsDialogProps) {
const { discoverSkills, SKILLS_DIR } = await import(
"../../agent/skills"
);
const { getSkillsDirectory, getNoSkills } = await import(
const { getSkillsDirectory, getSkillSources } = await import(
"../../agent/context"
);
const { join } = await import("node:path");
const skillsDir =
getSkillsDirectory() || join(process.cwd(), SKILLS_DIR);
const result = await discoverSkills(skillsDir, agentId, {
skipBundled: getNoSkills(),
sources: getSkillSources(),
});
setSkills(result.skills);
} catch {

View File

@@ -21,9 +21,10 @@ import { getClient } from "./agent/client";
import { setAgentContext, setConversationId } from "./agent/context";
import { createAgent } from "./agent/create";
import { ISOLATED_BLOCK_LABELS } from "./agent/memory";
import { sendMessageStream } from "./agent/message";
import { getModelUpdateArgs } from "./agent/model";
import { resolveSkillSourcesSelection } from "./agent/skillSources";
import type { SkillSource } from "./agent/skills";
import { SessionStats } from "./agent/stats";
import {
createBuffers,
@@ -33,6 +34,13 @@ import {
} from "./cli/helpers/accumulator";
import { classifyApprovals } from "./cli/helpers/approvalClassification";
import { formatErrorDetails } from "./cli/helpers/errorFormatter";
import {
getReflectionSettings,
type ReflectionBehavior,
type ReflectionSettings,
type ReflectionTrigger,
reflectionSettingsToLegacyMode,
} from "./cli/helpers/memoryReminder";
import {
type DrainStreamHook,
drainStreamWithResume,
@@ -113,11 +121,128 @@ export function shouldReinjectSkillsAfterCompaction(lines: Line[]): boolean {
);
}
type ReflectionOverrides = {
trigger?: ReflectionTrigger;
behavior?: ReflectionBehavior;
stepCount?: number;
};
function parseReflectionOverrides(
values: Record<string, unknown>,
): ReflectionOverrides {
const triggerRaw = values["reflection-trigger"] as string | undefined;
const behaviorRaw = values["reflection-behavior"] as string | undefined;
const stepCountRaw = values["reflection-step-count"] as string | undefined;
if (!triggerRaw && !behaviorRaw && !stepCountRaw) {
return {};
}
const overrides: ReflectionOverrides = {};
if (triggerRaw !== undefined) {
if (
triggerRaw !== "off" &&
triggerRaw !== "step-count" &&
triggerRaw !== "compaction-event"
) {
throw new Error(
`Invalid --reflection-trigger "${triggerRaw}". Valid values: off, step-count, compaction-event`,
);
}
overrides.trigger = triggerRaw;
}
if (behaviorRaw !== undefined) {
if (behaviorRaw !== "reminder" && behaviorRaw !== "auto-launch") {
throw new Error(
`Invalid --reflection-behavior "${behaviorRaw}". Valid values: reminder, auto-launch`,
);
}
overrides.behavior = behaviorRaw;
}
if (stepCountRaw !== undefined) {
const parsed = Number.parseInt(stepCountRaw, 10);
if (Number.isNaN(parsed) || parsed <= 0) {
throw new Error(
`Invalid --reflection-step-count "${stepCountRaw}". Expected a positive integer.`,
);
}
overrides.stepCount = parsed;
}
return overrides;
}
function hasReflectionOverrides(overrides: ReflectionOverrides): boolean {
return (
overrides.trigger !== undefined ||
overrides.behavior !== undefined ||
overrides.stepCount !== undefined
);
}
async function applyReflectionOverrides(
agentId: string,
overrides: ReflectionOverrides,
): Promise<ReflectionSettings> {
const current = getReflectionSettings();
const merged: ReflectionSettings = {
trigger: overrides.trigger ?? current.trigger,
behavior: overrides.behavior ?? current.behavior,
stepCount: overrides.stepCount ?? current.stepCount,
};
if (!hasReflectionOverrides(overrides)) {
return merged;
}
const memfsEnabled = settingsManager.isMemfsEnabled(agentId);
if (!memfsEnabled && merged.trigger === "compaction-event") {
throw new Error(
"--reflection-trigger compaction-event requires memfs enabled for this agent.",
);
}
if (
!memfsEnabled &&
merged.trigger !== "off" &&
merged.behavior === "auto-launch"
) {
throw new Error(
"--reflection-behavior auto-launch requires memfs enabled for this agent.",
);
}
try {
settingsManager.getLocalProjectSettings();
} catch {
await settingsManager.loadLocalProjectSettings();
}
const legacyMode = reflectionSettingsToLegacyMode(merged);
settingsManager.updateLocalProjectSettings({
memoryReminderInterval: legacyMode,
reflectionTrigger: merged.trigger,
reflectionBehavior: merged.behavior,
reflectionStepCount: merged.stepCount,
});
settingsManager.updateSettings({
memoryReminderInterval: legacyMode,
reflectionTrigger: merged.trigger,
reflectionBehavior: merged.behavior,
reflectionStepCount: merged.stepCount,
});
return merged;
}
export async function handleHeadlessCommand(
argv: string[],
model?: string,
skillsDirectory?: string,
noSkills?: boolean,
skillsDirectoryOverride?: string,
skillSourcesOverride?: SkillSource[],
systemInfoReminderEnabledOverride?: boolean,
) {
// Parse CLI args
// Include all flags from index.ts to prevent them from being treated as positionals
@@ -155,6 +280,7 @@ export async function handleHeadlessCommand(
"permission-mode": { type: "string" },
yolo: { type: "boolean" },
skills: { type: "string" },
"skill-sources": { type: "string" },
"pre-load-skills": { type: "string" },
"init-blocks": { type: "string" },
"base-tools": { type: "string" },
@@ -164,6 +290,11 @@ export async function handleHeadlessCommand(
memfs: { type: "boolean" },
"no-memfs": { type: "boolean" },
"no-skills": { type: "boolean" },
"no-bundled-skills": { type: "boolean" },
"no-system-info-reminder": { type: "boolean" },
"reflection-trigger": { type: "string" },
"reflection-behavior": { type: "string" },
"reflection-step-count": { type: "string" },
"max-turns": { type: "string" }, // Maximum number of agentic turns
},
strict: false,
@@ -299,6 +430,13 @@ export async function handleHeadlessCommand(
const blockValueArgs = values["block-value"] as string[] | undefined;
const initBlocksRaw = values["init-blocks"] as string | undefined;
const baseToolsRaw = values["base-tools"] as string | undefined;
const skillsDirectory =
(values.skills as string | undefined) ?? skillsDirectoryOverride;
const noSkillsFlag = values["no-skills"] as boolean | undefined;
const noBundledSkillsFlag = values["no-bundled-skills"] as
| boolean
| undefined;
const skillSourcesRaw = values["skill-sources"] as string | undefined;
const memfsFlag = values.memfs as boolean | undefined;
const noMemfsFlag = values["no-memfs"] as boolean | undefined;
const requestedMemoryPromptMode: "memfs" | "standard" | undefined = memfsFlag
@@ -309,8 +447,38 @@ export async function handleHeadlessCommand(
const shouldAutoEnableMemfsForNewAgent = !memfsFlag && !noMemfsFlag;
const fromAfFile = values["from-af"] as string | undefined;
const preLoadSkillsRaw = values["pre-load-skills"] as string | undefined;
const systemInfoReminderEnabled =
systemInfoReminderEnabledOverride ??
!(values["no-system-info-reminder"] as boolean | undefined);
const reflectionOverrides = (() => {
try {
return parseReflectionOverrides(values);
} catch (error) {
console.error(
error instanceof Error ? `Error: ${error.message}` : String(error),
);
process.exit(1);
}
})();
const maxTurnsRaw = values["max-turns"] as string | undefined;
const tagsRaw = values.tags as string | undefined;
const resolvedSkillSources = (() => {
if (skillSourcesOverride) {
return skillSourcesOverride;
}
try {
return resolveSkillSourcesSelection({
skillSourcesRaw,
noSkills: noSkillsFlag,
noBundledSkills: noBundledSkillsFlag,
});
} catch (error) {
console.error(
error instanceof Error ? `Error: ${error.message}` : String(error),
);
process.exit(1);
}
})();
// Parse and validate base tools
let tags: string[] | undefined;
@@ -339,6 +507,13 @@ export async function handleHeadlessCommand(
maxTurns = parsed;
}
if (preLoadSkillsRaw && resolvedSkillSources.length === 0) {
console.error(
"Error: --pre-load-skills cannot be used when all skill sources are disabled.",
);
process.exit(1);
}
// Handle --conv {agent-id} shorthand: --conv agent-xyz → --agent agent-xyz --conv default
if (specifiedConversationId?.startsWith("agent-")) {
if (specifiedAgentId && specifiedAgentId !== specifiedConversationId) {
@@ -748,6 +923,7 @@ export async function handleHeadlessCommand(
// Determine which conversation to use
let conversationId: string;
let effectiveReflectionSettings: ReflectionSettings;
const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent";
@@ -773,6 +949,18 @@ export async function handleHeadlessCommand(
process.exit(1);
}
try {
effectiveReflectionSettings = await applyReflectionOverrides(
agent.id,
reflectionOverrides,
);
} catch (error) {
console.error(
`Failed to apply sleeptime settings: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
// Determine which blocks to isolate for the conversation
const isolatedBlockLabels: string[] =
initBlocks === undefined
@@ -894,7 +1082,7 @@ export async function handleHeadlessCommand(
}
// Set agent context for tools that need it (e.g., Skill tool, Task tool)
setAgentContext(agent.id, skillsDirectory, noSkills);
setAgentContext(agent.id, skillsDirectory, resolvedSkillSources);
// Validate output format
const outputFormat =
@@ -929,6 +1117,9 @@ export async function handleHeadlessCommand(
outputFormat,
includePartialMessages,
availableTools,
resolvedSkillSources,
systemInfoReminderEnabled,
effectiveReflectionSettings,
);
return;
}
@@ -957,6 +1148,11 @@ export async function handleHeadlessCommand(
permission_mode: "",
slash_commands: [],
memfs_enabled: settingsManager.isMemfsEnabled(agent.id),
skill_sources: resolvedSkillSources,
system_info_reminder_enabled: systemInfoReminderEnabled,
reflection_trigger: effectiveReflectionSettings.trigger,
reflection_behavior: effectiveReflectionSettings.behavior,
reflection_step_count: effectiveReflectionSettings.stepCount,
uuid: `init-${agent.id}`,
};
console.log(JSON.stringify(initEvent));
@@ -1160,7 +1356,7 @@ ${SYSTEM_REMINDER_CLOSE}
try {
const skillsDir = getSkillsDirectory() || join(process.cwd(), defaultDir);
const { skills } = await discoverSkills(skillsDir, agent.id, {
skipBundled: noSkills,
sources: resolvedSkillSources,
});
const skillsReminder = formatSkillsAsSystemReminder(skills);
if (skillsReminder) {
@@ -2027,6 +2223,9 @@ async function runBidirectionalMode(
_outputFormat: string,
includePartialMessages: boolean,
availableTools: string[],
skillSources: SkillSource[],
systemInfoReminderEnabled: boolean,
reflectionSettings: ReflectionSettings,
): Promise<void> {
const sessionId = agent.id;
const readline = await import("node:readline");
@@ -2042,6 +2241,11 @@ async function runBidirectionalMode(
tools: availableTools,
cwd: process.cwd(),
memfs_enabled: settingsManager.isMemfsEnabled(agent.id),
skill_sources: skillSources,
system_info_reminder_enabled: systemInfoReminderEnabled,
reflection_trigger: reflectionSettings.trigger,
reflection_behavior: reflectionSettings.behavior,
reflection_step_count: reflectionSettings.stepCount,
uuid: `init-${agent.id}`,
};
console.log(JSON.stringify(initEvent));
@@ -2355,6 +2559,12 @@ async function runBidirectionalMode(
agent_id: agent.id,
model: agent.llm_config?.model,
tools: availableTools,
memfs_enabled: settingsManager.isMemfsEnabled(agent.id),
skill_sources: skillSources,
system_info_reminder_enabled: systemInfoReminderEnabled,
reflection_trigger: reflectionSettings.trigger,
reflection_behavior: reflectionSettings.behavior,
reflection_step_count: reflectionSettings.stepCount,
},
},
session_id: sessionId,
@@ -2485,7 +2695,9 @@ async function runBidirectionalMode(
const { join } = await import("node:path");
const skillsDir =
getSkillsDirectory() || join(process.cwd(), defaultDir);
const { skills } = await discover(skillsDir, agent.id);
const { skills } = await discover(skillsDir, agent.id, {
sources: skillSources,
});
const latestSkillsReminder = formatSkillsAsSystemReminder(skills);
// Trigger reinjection when the available-skills block changed on disk.

View File

@@ -12,6 +12,7 @@ import {
import type { AgentProvenance } from "./agent/create";
import { getLettaCodeHeaders } from "./agent/http-headers";
import { ISOLATED_BLOCK_LABELS } from "./agent/memory";
import { resolveSkillSourcesSelection } from "./agent/skillSources";
import { LETTA_CLOUD_API_URL } from "./auth/oauth";
import { ConversationSelector } from "./cli/components/ConversationSelector";
import type { ApprovalRequest } from "./cli/helpers/stream";
@@ -78,10 +79,21 @@ OPTIONS
Emit stream_event wrappers for each chunk (stream-json only)
--from-agent <id> Inject agent-to-agent system reminder (headless mode)
--skills <path> Custom path to skills directory (default: .skills in current directory)
--skill-sources <csv> Skill sources: all,bundled,global,agent,project (default: all)
--no-skills Disable all skill sources
--no-bundled-skills Disable bundled skills only
--import <path> Create agent from an AgentFile (.af) template
Use @author/name to import from the agent registry
--memfs Enable memory filesystem for this agent
--no-memfs Disable memory filesystem for this agent
--no-system-info-reminder
Disable first-turn environment reminder (device/git/cwd context)
--reflection-trigger <mode>
Sleeptime trigger: off, step-count, compaction-event
--reflection-behavior <mode>
Sleeptime behavior: reminder, auto-launch
--reflection-step-count <n>
Sleeptime step-count interval (positive integer)
SUBCOMMANDS (JSON-only)
letta memfs status --agent <id>
@@ -423,6 +435,7 @@ async function main(): Promise<void> {
"include-partial-messages": { type: "boolean" },
"from-agent": { type: "string" },
skills: { type: "string" },
"skill-sources": { type: "string" },
"pre-load-skills": { type: "string" },
"from-af": { type: "string" },
import: { type: "string" },
@@ -431,6 +444,11 @@ async function main(): Promise<void> {
memfs: { type: "boolean" },
"no-memfs": { type: "boolean" },
"no-skills": { type: "boolean" },
"no-bundled-skills": { type: "boolean" },
"no-system-info-reminder": { type: "boolean" },
"reflection-trigger": { type: "string" },
"reflection-behavior": { type: "string" },
"reflection-step-count": { type: "string" },
"max-turns": { type: "string" },
},
strict: true,
@@ -559,6 +577,27 @@ async function main(): Promise<void> {
: undefined;
const shouldAutoEnableMemfsForNewAgent = !memfsFlag && !noMemfsFlag;
const noSkillsFlag = values["no-skills"] as boolean | undefined;
const noBundledSkillsFlag = values["no-bundled-skills"] as
| boolean
| undefined;
const skillSourcesRaw = values["skill-sources"] as string | undefined;
const noSystemInfoReminderFlag = values["no-system-info-reminder"] as
| boolean
| undefined;
const resolvedSkillSources = (() => {
try {
return resolveSkillSourcesSelection({
skillSourcesRaw,
noSkills: noSkillsFlag,
noBundledSkills: noBundledSkillsFlag,
});
} catch (error) {
console.error(
error instanceof Error ? `Error: ${error.message}` : String(error),
);
process.exit(1);
}
})();
const fromAfFile =
(values.import as string | undefined) ??
(values["from-af"] as string | undefined);
@@ -956,7 +995,8 @@ async function main(): Promise<void> {
process.argv,
specifiedModel,
skillsDirectory,
noSkillsFlag,
resolvedSkillSources,
!noSystemInfoReminderFlag,
);
return;
}
@@ -1724,7 +1764,7 @@ async function main(): Promise<void> {
}
// Set agent context for tools that need it (e.g., Skill tool)
setAgentContext(agent.id, skillsDirectory, noSkillsFlag);
setAgentContext(agent.id, skillsDirectory, resolvedSkillSources);
// Apply memfs flag if explicitly specified (memfs is opt-in via /memfs enable or --memfs)
const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent";
@@ -2064,6 +2104,7 @@ async function main(): Promise<void> {
showCompactions: settings.showCompactions,
agentProvenance,
releaseNotes,
sessionContextReminderEnabled: !noSystemInfoReminderFlag,
});
}
@@ -2081,6 +2122,7 @@ async function main(): Promise<void> {
showCompactions: settings.showCompactions,
agentProvenance,
releaseNotes,
sessionContextReminderEnabled: !noSystemInfoReminderFlag,
});
}

View File

@@ -0,0 +1,51 @@
import { describe, expect, test } from "bun:test";
import {
ALL_SKILL_SOURCES,
parseSkillSourcesList,
resolveSkillSourcesSelection,
} from "../../agent/skillSources";
describe("skill source selection", () => {
test("defaults to all sources", () => {
expect(resolveSkillSourcesSelection({})).toEqual(ALL_SKILL_SOURCES);
});
test("--no-skills disables all sources", () => {
expect(
resolveSkillSourcesSelection({
noSkills: true,
}),
).toEqual([]);
});
test("--no-bundled-skills removes bundled from default set", () => {
expect(
resolveSkillSourcesSelection({
noBundledSkills: true,
}),
).toEqual(["global", "agent", "project"]);
});
test("--skill-sources accepts explicit subsets and normalizes order", () => {
expect(parseSkillSourcesList("project,global")).toEqual([
"global",
"project",
]);
});
test("--skill-sources supports all keyword", () => {
expect(parseSkillSourcesList("all,project")).toEqual(ALL_SKILL_SOURCES);
});
test("throws for invalid source", () => {
expect(() => parseSkillSourcesList("project,unknown")).toThrow(
'Invalid skill source "unknown"',
);
});
test("throws for empty --skill-sources value", () => {
expect(() => parseSkillSourcesList(" , ")).toThrow(
"--skill-sources must include at least one source",
);
});
});

View File

@@ -89,6 +89,11 @@ export interface SystemInitMessage extends MessageEnvelope {
permission_mode: string;
slash_commands: string[];
memfs_enabled?: boolean;
skill_sources?: Array<"bundled" | "global" | "agent" | "project">;
system_info_reminder_enabled?: boolean;
reflection_trigger?: "off" | "step-count" | "compaction-event";
reflection_behavior?: "reminder" | "auto-launch";
reflection_step_count?: number;
// output_style omitted - Letta Code doesn't have output styles feature
}