feat: expose skill source and sleeptime controls in SDK (#43)

This commit is contained in:
Charles Packer
2026-02-16 23:22:52 -08:00
committed by GitHub
parent 0abacd101d
commit b6ffe5ea91
9 changed files with 383 additions and 5 deletions

View File

@@ -44,6 +44,11 @@ export type {
SDKReasoningMessage,
SDKResultMessage,
SDKStreamEventMessage,
SkillSource,
SleeptimeOptions,
SleeptimeTrigger,
SleeptimeBehavior,
EffectiveSleeptimeSettings,
PermissionMode,
CanUseToolCallback,
CanUseToolResponse,

View File

@@ -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<string, unknown> = {},
): 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,

View File

@@ -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,
};
}

View File

@@ -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");
});
});

View File

@@ -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;

View File

@@ -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 {

64
src/validation.test.ts Normal file
View File

@@ -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");
});
});

View File

@@ -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);
}