feat: add skill source controls and headless reminder settings (#989)
This commit is contained in:
@@ -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
82
src/agent/skillSources.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
224
src/headless.ts
224
src/headless.ts
@@ -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.
|
||||
|
||||
46
src/index.ts
46
src/index.ts
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
51
src/tests/agent/skill-sources.test.ts
Normal file
51
src/tests/agent/skill-sources.test.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user