feat: add client side skills (#1320)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Sarah Wooders
2026-03-10 13:18:14 -07:00
committed by GitHub
parent 87312720d5
commit e82a2d33f8
25 changed files with 377 additions and 151 deletions

View File

@@ -7,7 +7,6 @@ export type SharedReminderMode =
export type SharedReminderId =
| "session-context"
| "agent-info"
| "skills"
| "permission-mode"
| "plan-mode"
| "reflection-step-count"
@@ -40,11 +39,6 @@ export const SHARED_REMINDER_CATALOG: ReadonlyArray<SharedReminderDefinition> =
"subagent",
],
},
{
id: "skills",
description: "Available skills system reminder (with reinjection)",
modes: ["interactive", "headless-one-shot", "headless-bidirectional"],
},
{
id: "permission-mode",
description: "Permission mode reminder",

View File

@@ -1,12 +1,5 @@
import { join } from "node:path";
import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents";
import { getSkillsDirectory } from "../agent/context";
import {
discoverSkills,
formatSkillsAsSystemReminder,
SKILLS_DIR,
type SkillSource,
} from "../agent/skills";
import type { SkillSource } from "../agent/skills";
import { buildAgentInfo } from "../cli/helpers/agentInfo";
import {
buildCompactionMemoryReminder,
@@ -101,52 +94,6 @@ async function buildSessionContextReminder(
return reminder || null;
}
async function buildSkillsReminder(
context: SharedReminderContext,
): Promise<string | null> {
const previousSkillsReminder = context.state.cachedSkillsReminder;
// Keep a stable empty baseline so a later successful discovery can diff
// against "" and trigger reinjection, even after an earlier discovery failure.
let latestSkillsReminder = previousSkillsReminder ?? "";
try {
const skillsDir = getSkillsDirectory() || join(process.cwd(), SKILLS_DIR);
const { skills } = await discoverSkills(skillsDir, context.agent.id, {
sources: context.skillSources,
});
latestSkillsReminder = formatSkillsAsSystemReminder(skills);
context.state.skillPathById = Object.fromEntries(
skills
.filter(
(skill) => typeof skill.path === "string" && skill.path.length > 0,
)
.map((skill) => [skill.id, skill.path as string]),
);
} catch {
// Keep previous snapshot when discovery fails.
}
if (
previousSkillsReminder !== null &&
previousSkillsReminder !== latestSkillsReminder
) {
context.state.pendingSkillsReinject = true;
}
context.state.cachedSkillsReminder = latestSkillsReminder;
const shouldInject =
!context.state.hasInjectedSkillsReminder ||
context.state.pendingSkillsReinject;
if (!shouldInject) {
return null;
}
context.state.hasInjectedSkillsReminder = true;
context.state.pendingSkillsReinject = false;
return latestSkillsReminder || null;
}
async function buildPlanModeReminder(
context: SharedReminderContext,
): Promise<string | null> {
@@ -398,7 +345,6 @@ export const sharedReminderProviders: Record<
> = {
"agent-info": buildAgentInfoReminder,
"session-context": buildSessionContextReminder,
skills: buildSkillsReminder,
"permission-mode": buildPermissionModeReminder,
"plan-mode": buildPlanModeReminder,
"reflection-step-count": buildReflectionStepReminder,

View File

@@ -22,12 +22,8 @@ export interface ToolsetChangeReminder {
export interface SharedReminderState {
hasSentAgentInfo: boolean;
hasSentSessionContext: boolean;
hasInjectedSkillsReminder: boolean;
cachedSkillsReminder: string | null;
skillPathById: Record<string, string>;
lastNotifiedPermissionMode: PermissionMode | null;
turnCount: number;
pendingSkillsReinject: boolean;
pendingReflectionTrigger: boolean;
pendingAutoInitReminder: boolean;
pendingCommandIoReminders: CommandIoReminder[];
@@ -40,12 +36,8 @@ export function createSharedReminderState(): SharedReminderState {
return {
hasSentAgentInfo: false,
hasSentSessionContext: false,
hasInjectedSkillsReminder: false,
cachedSkillsReminder: null,
skillPathById: {},
lastNotifiedPermissionMode: null,
turnCount: 0,
pendingSkillsReinject: false,
pendingReflectionTrigger: false,
pendingAutoInitReminder: false,
pendingCommandIoReminders: [],
@@ -63,10 +55,6 @@ export function syncReminderStateFromContextTracker(
state: SharedReminderState,
contextTracker: ContextTracker,
): void {
if (contextTracker.pendingSkillsReinject) {
state.pendingSkillsReinject = true;
contextTracker.pendingSkillsReinject = false;
}
if (contextTracker.pendingReflectionTrigger) {
state.pendingReflectionTrigger = true;
contextTracker.pendingReflectionTrigger = false;