diff --git a/README.md b/README.md index 48bf4cc..0c4b35e 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,25 @@ for await (const msg of session.stream()) { By default, `resumeSession(agentId)` continues the agent’s default conversation. To start a fresh thread, use `createSession(agentId)` (see docs). +## Session configuration + +The SDK surfaces the same runtime controls as Letta Code CLI for skills, reminders, and sleeptime: + +```ts +import { createSession } from "@letta-ai/letta-code-sdk"; + +const session = createSession("agent-123", { + skillSources: ["project", "global"], // [] disables all skills (--no-skills) + systemInfoReminder: false, // maps to --no-system-info-reminder + sleeptime: { + trigger: "step-count", // off | step-count | compaction-event + behavior: "reminder", // reminder | auto-launch + stepCount: 8, + }, + memfs: true, // true -> --memfs, false -> --no-memfs +}); +``` + ## Links - Docs: https://docs.letta.com/letta-code-sdk diff --git a/src/index.ts b/src/index.ts index 6486991..a2788a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,11 @@ export type { SDKReasoningMessage, SDKResultMessage, SDKStreamEventMessage, + SkillSource, + SleeptimeOptions, + SleeptimeTrigger, + SleeptimeBehavior, + EffectiveSleeptimeSettings, PermissionMode, CanUseToolCallback, CanUseToolResponse, diff --git a/src/session.test.ts b/src/session.test.ts index f603d4a..3e22b1b 100644 --- a/src/session.test.ts +++ b/src/session.test.ts @@ -72,7 +72,9 @@ function attachMockTransport(session: Session, transport: MockTransport): void { (session as unknown as { transport: MockTransport }).transport = transport; } -function createInitMessage(): WireMessage { +function createInitMessage( + overrides: Record = {}, +): WireMessage { return { type: "system", subtype: "init", @@ -81,6 +83,7 @@ function createInitMessage(): WireMessage { conversation_id: "conversation-1", model: "claude-sonnet-4", tools: ["Bash"], + ...overrides, } as WireMessage; } @@ -148,6 +151,37 @@ async function waitFor( } describe("Session", () => { + test("initialize returns optional init settings when provided by CLI", async () => { + const session = new Session(); + const transport = new MockTransport(); + attachMockTransport(session, transport); + + try { + transport.push( + createInitMessage({ + memfs_enabled: true, + skill_sources: ["project", "agent"], + system_info_reminder_enabled: false, + reflection_trigger: "step-count", + reflection_behavior: "reminder", + reflection_step_count: 9, + }), + ); + + const init = await session.initialize(); + expect(init.memfsEnabled).toBe(true); + expect(init.skillSources).toEqual(["project", "agent"]); + expect(init.systemInfoReminderEnabled).toBe(false); + expect(init.sleeptime).toEqual({ + trigger: "step-count", + behavior: "reminder", + stepCount: 9, + }); + } finally { + session.close(); + } + }); + describe("handleCanUseTool with bypassPermissions", () => { async function invokeCanUseTool( session: Session, diff --git a/src/session.ts b/src/session.ts index 8c7ef47..e8084bd 100644 --- a/src/session.ts +++ b/src/session.ts @@ -102,6 +102,12 @@ export class Session implements AsyncDisposable { conversation_id: string; model: string; tools: 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; }; this._agentId = initMsg.agent_id; this._sessionId = initMsg.session_id; @@ -129,6 +135,19 @@ export class Session implements AsyncDisposable { conversationId: initMsg.conversation_id, model: initMsg.model, tools: allTools, + memfsEnabled: initMsg.memfs_enabled, + skillSources: initMsg.skill_sources, + systemInfoReminderEnabled: initMsg.system_info_reminder_enabled, + sleeptime: + initMsg.reflection_trigger && + initMsg.reflection_behavior && + typeof initMsg.reflection_step_count === "number" + ? { + trigger: initMsg.reflection_trigger, + behavior: initMsg.reflection_behavior, + stepCount: initMsg.reflection_step_count, + } + : undefined, }; } } @@ -569,6 +588,12 @@ export class Session implements AsyncDisposable { conversation_id: string; model: string; tools: 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; }; return { type: "init", @@ -577,6 +602,19 @@ export class Session implements AsyncDisposable { conversationId: msg.conversation_id, model: msg.model, tools: msg.tools, + memfsEnabled: msg.memfs_enabled, + skillSources: msg.skill_sources, + systemInfoReminderEnabled: msg.system_info_reminder_enabled, + sleeptime: + msg.reflection_trigger && + msg.reflection_behavior && + typeof msg.reflection_step_count === "number" + ? { + trigger: msg.reflection_trigger, + behavior: msg.reflection_behavior, + stepCount: msg.reflection_step_count, + } + : undefined, }; } diff --git a/src/transport.test.ts b/src/transport.test.ts index f5ae948..86832de 100644 --- a/src/transport.test.ts +++ b/src/transport.test.ts @@ -31,6 +31,13 @@ describe("transport args", () => { allowedTools?: string[]; disallowedTools?: string[]; memfs?: boolean; + skillSources?: Array<"bundled" | "global" | "agent" | "project">; + systemInfoReminder?: boolean; + sleeptime?: { + trigger?: "off" | "step-count" | "compaction-event"; + behavior?: "reminder" | "auto-launch"; + stepCount?: number; + }; } = {}): string[] { const transport = new SubprocessTransport(options); // Access private helper for deterministic argument testing. @@ -73,8 +80,48 @@ describe("transport args", () => { expect(args).toContain("--memfs"); }); - test("memfs false/undefined does not forward --memfs", () => { - expect(buildArgsFor({ memfs: false })).not.toContain("--memfs"); + test("memfs false forwards --no-memfs", () => { + const args = buildArgsFor({ memfs: false }); + expect(args).toContain("--no-memfs"); + expect(args).not.toContain("--memfs"); + }); + + test("memfs undefined does not forward memfs flags", () => { expect(buildArgsFor({})).not.toContain("--memfs"); + expect(buildArgsFor({})).not.toContain("--no-memfs"); + }); + + test("empty skillSources forwards --no-skills", () => { + const args = buildArgsFor({ skillSources: [] }); + expect(args).toContain("--no-skills"); + expect(args).not.toContain("--skill-sources"); + }); + + test("skillSources list forwards --skill-sources csv", () => { + const args = buildArgsFor({ skillSources: ["project", "global"] }); + expect(args).toContain("--skill-sources"); + expect(args).toContain("project,global"); + expect(args).not.toContain("--no-skills"); + }); + + test("systemInfoReminder false forwards --no-system-info-reminder", () => { + const args = buildArgsFor({ systemInfoReminder: false }); + expect(args).toContain("--no-system-info-reminder"); + }); + + test("sleeptime options forward reflection flags", () => { + const args = buildArgsFor({ + sleeptime: { + trigger: "step-count", + behavior: "reminder", + stepCount: 12, + }, + }); + expect(args).toContain("--reflection-trigger"); + expect(args).toContain("step-count"); + expect(args).toContain("--reflection-behavior"); + expect(args).toContain("reminder"); + expect(args).toContain("--reflection-step-count"); + expect(args).toContain("12"); }); }); diff --git a/src/transport.ts b/src/transport.ts index 3dd4b3e..f4f7615 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -368,8 +368,39 @@ export class SubprocessTransport { } // Memory filesystem - if (this.options.memfs) { + if (this.options.memfs === true) { args.push("--memfs"); + } else if (this.options.memfs === false) { + args.push("--no-memfs"); + } + + // Skills sources + if (this.options.skillSources !== undefined) { + const sources = [...new Set(this.options.skillSources)]; + if (sources.length === 0) { + args.push("--no-skills"); + } else { + args.push("--skill-sources", sources.join(",")); + } + } + + // Session context reminder toggle + if (this.options.systemInfoReminder === false) { + args.push("--no-system-info-reminder"); + } + + // Sleeptime/reflection settings + if (this.options.sleeptime?.trigger !== undefined) { + args.push("--reflection-trigger", this.options.sleeptime.trigger); + } + if (this.options.sleeptime?.behavior !== undefined) { + args.push("--reflection-behavior", this.options.sleeptime.behavior); + } + if (this.options.sleeptime?.stepCount !== undefined) { + args.push( + "--reflection-step-count", + String(this.options.sleeptime.stepCount), + ); } return args; diff --git a/src/types.ts b/src/types.ts index ac9cc72..8640802 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,6 +61,35 @@ export type MessageContentItem = TextContent | ImageContent; */ export type SendMessage = string | MessageContentItem[]; +// ═══════════════════════════════════════════════════════════════ +// SKILLS / REMINDER / SLEEPTIME TYPES +// ═══════════════════════════════════════════════════════════════ + +export type SkillSource = "bundled" | "global" | "agent" | "project"; + +export type SleeptimeTrigger = "off" | "step-count" | "compaction-event"; + +export type SleeptimeBehavior = "reminder" | "auto-launch"; + +/** + * Sleeptime settings exposed through SDK options. + * Any omitted fields preserve server/CLI defaults. + */ +export interface SleeptimeOptions { + trigger?: SleeptimeTrigger; + behavior?: SleeptimeBehavior; + stepCount?: number; +} + +/** + * Fully-resolved sleeptime settings emitted by init messages. + */ +export interface EffectiveSleeptimeSettings { + trigger: SleeptimeTrigger; + behavior: SleeptimeBehavior; + stepCount: number; +} + // ═══════════════════════════════════════════════════════════════ // SYSTEM PROMPT TYPES // ═══════════════════════════════════════════════════════════════ @@ -214,6 +243,11 @@ export interface InternalSessionOptions { // Memory filesystem (only for new agents) memfs?: boolean; + // Skills/reminders + skillSources?: SkillSource[]; + systemInfoReminder?: boolean; + sleeptime?: SleeptimeOptions; + // Permissions allowedTools?: string[]; disallowedTools?: string[]; @@ -256,6 +290,29 @@ export interface CreateSessionOptions { /** Working directory for the CLI process */ cwd?: string; + /** + * Enable/disable memory filesystem for this agent before running. + * true -> `--memfs`, false -> `--no-memfs`, undefined -> leave unchanged. + */ + memfs?: boolean; + + /** + * Restrict available skills by source. + * Empty array disables all skills (`--no-skills`). + */ + skillSources?: SkillSource[]; + + /** + * Toggle first-turn system info reminder (device/git/cwd context). + * false -> `--no-system-info-reminder`. + */ + systemInfoReminder?: boolean; + + /** + * Configure sleeptime (reflection) settings, equivalent to `/sleeptime`. + */ + sleeptime?: SleeptimeOptions; + /** Custom permission callback - called when tool needs approval */ canUseTool?: CanUseToolCallback; @@ -327,6 +384,23 @@ export interface CreateAgentOptions { * Maps to Letta Code CLI `--memfs` during agent creation. */ memfs?: boolean; + + /** + * Restrict available skills by source. + * Empty array disables all skills (`--no-skills`). + */ + skillSources?: SkillSource[]; + + /** + * Toggle first-turn system info reminder (device/git/cwd context). + * false -> `--no-system-info-reminder`. + */ + systemInfoReminder?: boolean; + + /** + * Configure sleeptime (reflection) settings, equivalent to `/sleeptime`. + */ + sleeptime?: SleeptimeOptions; } // ═══════════════════════════════════════════════════════════════ @@ -343,6 +417,10 @@ export interface SDKInitMessage { conversationId: string; model: string; tools: string[]; + memfsEnabled?: boolean; + skillSources?: SkillSource[]; + systemInfoReminderEnabled?: boolean; + sleeptime?: EffectiveSleeptimeSettings; } export interface SDKAssistantMessage { diff --git a/src/validation.test.ts b/src/validation.test.ts new file mode 100644 index 0000000..bba7c34 --- /dev/null +++ b/src/validation.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from "bun:test"; +import { validateCreateAgentOptions, validateCreateSessionOptions } from "./validation.js"; + +describe("validation", () => { + test("accepts valid session skill/reminder/sleeptime options", () => { + expect(() => + validateCreateSessionOptions({ + skillSources: ["project", "global"], + systemInfoReminder: false, + sleeptime: { + trigger: "step-count", + behavior: "reminder", + stepCount: 6, + }, + }), + ).not.toThrow(); + }); + + test("rejects invalid session skill source", () => { + expect(() => + validateCreateSessionOptions({ + // biome-ignore lint/suspicious/noExplicitAny: runtime validation test + skillSources: ["invalid-source"] as any, + }), + ).toThrow("Invalid skill source"); + }); + + test("rejects invalid session sleeptime options", () => { + expect(() => + validateCreateSessionOptions({ + sleeptime: { + // biome-ignore lint/suspicious/noExplicitAny: runtime validation test + trigger: "sometimes" as any, + }, + }), + ).toThrow("Invalid sleeptime.trigger"); + + expect(() => + validateCreateSessionOptions({ + sleeptime: { + // biome-ignore lint/suspicious/noExplicitAny: runtime validation test + behavior: "manual" as any, + }, + }), + ).toThrow("Invalid sleeptime.behavior"); + + expect(() => + validateCreateSessionOptions({ + sleeptime: { + stepCount: 0, + }, + }), + ).toThrow("Invalid sleeptime.stepCount"); + }); + + test("rejects invalid agent skill source", () => { + expect(() => + validateCreateAgentOptions({ + // biome-ignore lint/suspicious/noExplicitAny: runtime validation test + skillSources: ["bundled", "bad"] as any, + }), + ).toThrow("Invalid skill source"); + }); +}); diff --git a/src/validation.ts b/src/validation.ts index def50b2..b8af533 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -9,9 +9,18 @@ import type { CreateAgentOptions, MemoryItem, CreateBlock, - SystemPromptPreset + SystemPromptPreset, + SkillSource, + SleeptimeOptions, } from "./types.js"; +const VALID_SKILL_SOURCES: SkillSource[] = [ + "bundled", + "global", + "agent", + "project", +]; + /** * Extract block labels from memory items. */ @@ -46,6 +55,53 @@ function validateSystemPromptPreset(preset: string): void { } } +function validateSkillSources(sources: SkillSource[] | undefined): void { + if (sources === undefined) { + return; + } + + for (const source of sources) { + if (!VALID_SKILL_SOURCES.includes(source)) { + throw new Error( + `Invalid skill source '${source}'. Valid values: ${VALID_SKILL_SOURCES.join(", ")}` + ); + } + } +} + +function validateSleeptimeOptions(sleeptime: SleeptimeOptions | undefined): void { + if (sleeptime === undefined) { + return; + } + + if ( + sleeptime.trigger !== undefined && + !["off", "step-count", "compaction-event"].includes(sleeptime.trigger) + ) { + throw new Error( + `Invalid sleeptime.trigger '${String(sleeptime.trigger)}'. Valid values: off, step-count, compaction-event` + ); + } + + if ( + sleeptime.behavior !== undefined && + !["reminder", "auto-launch"].includes(sleeptime.behavior) + ) { + throw new Error( + `Invalid sleeptime.behavior '${String(sleeptime.behavior)}'. Valid values: reminder, auto-launch` + ); + } + + if ( + sleeptime.stepCount !== undefined && + (!Number.isInteger(sleeptime.stepCount) || sleeptime.stepCount <= 0) + ) { + throw new Error( + "Invalid sleeptime.stepCount. Expected a positive integer." + ); + } +} + /** * Validate CreateSessionOptions (used by createSession and resumeSession). */ @@ -54,6 +110,9 @@ export function validateCreateSessionOptions(options: CreateSessionOptions): voi if (options.systemPrompt !== undefined) { validateSystemPromptPreset(options.systemPrompt); } + + validateSkillSources(options.skillSources); + validateSleeptimeOptions(options.sleeptime); } /** @@ -104,4 +163,7 @@ export function validateCreateAgentOptions(options: CreateAgentOptions): void { } // If not a preset, it's a custom string - no validation needed } + + validateSkillSources(options.skillSources); + validateSleeptimeOptions(options.sleeptime); }