feat: expose skill source and sleeptime controls in SDK (#43)
This commit is contained in:
@@ -44,6 +44,11 @@ export type {
|
||||
SDKReasoningMessage,
|
||||
SDKResultMessage,
|
||||
SDKStreamEventMessage,
|
||||
SkillSource,
|
||||
SleeptimeOptions,
|
||||
SleeptimeTrigger,
|
||||
SleeptimeBehavior,
|
||||
EffectiveSleeptimeSettings,
|
||||
PermissionMode,
|
||||
CanUseToolCallback,
|
||||
CanUseToolResponse,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
78
src/types.ts
78
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 {
|
||||
|
||||
64
src/validation.test.ts
Normal file
64
src/validation.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user