Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ An agent is the persistent entity with memory. A conversation is a thread on tha
- `resumeSession(conversationId)` resumes a saved conversation.
- `resumeSession(agentId)` resumes the agent's default conversation.

Pass `stateless: true` when a session should use an existing agent without
loading or changing its MemFS:

```ts
await using session = client.createSession(agentId, { stateless: true });
```

The agent and conversation still persist. Stateless sessions preserve the
agent's model, prompt, tools, tags, and sampling settings, but skip MemFS sync,
agent-scoped skills and mods, memory transcript writes, and reflection for that
session. Options that mutate persisted configuration (`model`,
`reasoningEffort`, `dreaming`, and `resources`) are rejected, as is
`session.updateModel()`. The option works with local, remote App Server, and
Cloud backends.

Portable sessions also expose the stateful controls needed by interactive
clients:

Expand Down
6 changes: 6 additions & 0 deletions src/app-server-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,12 @@ export class AppServerSession extends RemoteClientSessionCore {
const mode = mapPermissionMode(options.permissionMode);
if (mode) command.mode = mode;
if (options.cwd !== undefined) command.cwd = options.cwd;
if (
this.mode.kind === "session" &&
this.mode.options.stateless === true
) {
command.stateless = true;
}
// Keep the distinction between omitted (use harness defaults) and []
// (disable bundled/global/agent/project skills). The app-server runtime is
// session-scoped, so this must be sent on creation and every resume.
Expand Down
1 change: 1 addition & 0 deletions src/cloud-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
const mode = mapPermissionMode(options.permissionMode);
if (mode) command.mode = mode;
if (options.cwd !== undefined) command.cwd = options.cwd;
if (this.cloudMode.options.stateless === true) command.stateless = true;
if (options.skillSources !== undefined) {
command.skill_sources = [...new Set(options.skillSources)];
}
Expand Down
9 changes: 8 additions & 1 deletion src/remote-client-session-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
}

async updateModel(update: string | UpdateModelOptions): Promise<UpdateModelResult> {
if (this.mode.kind === "session" && this.mode.options.stateless === true) {
throw new Error(
"updateModel() is unavailable in a stateless session because it changes persisted agent configuration.",
);
}
if (!this.initialized) {
await this.initialize();
}
Expand Down Expand Up @@ -779,7 +784,9 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
}

const dreamingSettings = resolveDreamingSettings(options.dreaming);
if (dreamingSettings) {
const isStatelessSession =
this.mode.kind === "session" && this.mode.options.stateless === true;
if (dreamingSettings && !isStatelessSession) {
const response = await this.controller.request(
"set_reflection_settings",
{
Expand Down
9 changes: 8 additions & 1 deletion src/tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,10 @@ describe("LettaAgentClient", () => {
appServer: { url: "ws://127.0.0.1:4500/ws", WebSocket: FakeAppServerSocket },
});

const session = client.createSession("agent-123", { cwd: "/tmp/project" });
const session = client.createSession("agent-123", {
cwd: "/tmp/project",
stateless: true,
});
try {
const init = await asAdvanced(session).initialize();
expect(init.agentId).toBe("agent-123");
Expand All @@ -792,7 +795,11 @@ describe("LettaAgentClient", () => {
agent_id: "agent-123",
create_conversation: { body: {} },
cwd: "/tmp/project",
stateless: true,
});
await expect(session.updateModel("openai/gpt-5.2")).rejects.toThrow(
"unavailable in a stateless session",
);
} finally {
session.close();
}
Expand Down
2 changes: 2 additions & 0 deletions src/tests/cloud-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2123,6 +2123,7 @@ describe("CloudEnvironmentSession", () => {
});

const session = client.resumeSession("agent-1", {
stateless: true,
tools: [
{
name: "lookup_ticket",
Expand Down Expand Up @@ -2158,6 +2159,7 @@ describe("CloudEnvironmentSession", () => {
const controlSocket = FakeCloudSocket.socket("control")!;
const runtimeStart = controlSocket.sent.find((command) => command.type === "runtime_start")!;
expect(runtimeStart).toMatchObject({
stateless: true,
external_tools: [
{
tools: expect.arrayContaining([
Expand Down
17 changes: 17 additions & 0 deletions src/tests/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ describe("validation", () => {
).toThrow("Invalid toolset.include");
});

test("validates stateless session options", () => {
expect(() =>
validateCreateSessionOptions({ stateless: "yes" } as never),
).toThrow("Invalid stateless");

for (const options of [
{ stateless: true, model: "openai/gpt-5.2" },
{ stateless: true, reasoningEffort: "high" },
{ stateless: true, dreaming: { trigger: "step-count" } },
{ stateless: true, resources: [] },
]) {
expect(() => validateCreateSessionOptions(options as never)).toThrow(
"changes persisted agent configuration",
);
}
});

test("rejects invalid session reasoning effort", () => {
expect(() =>
validateCreateSessionOptions({
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,13 @@ export interface CreateSessionOptions {
/** Working directory for the CLI process */
cwd?: string;

/**
* Run without loading or changing the agent's MemFS. The agent and
* conversation remain persistent; this only changes the session's local
* memory, agent-skill, agent-mod, transcript, and reflection behavior.
*/
stateless?: boolean;

/**
* Restrict available skills by source.
* Empty array disables all skills (`--no-skills`).
Expand Down
16 changes: 16 additions & 0 deletions src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,22 @@ function validateMcpServers(servers: McpServers | undefined): void {
* Validate CreateSessionOptions (used by createSession and resumeSession).
*/
export function validateCreateSessionOptions(options: CreateSessionOptions): void {
if (options.stateless !== undefined && typeof options.stateless !== "boolean") {
throw new Error("Invalid stateless. Expected a boolean.");
}
if (options.stateless) {
const persistedOption = [
["model", options.model],
["reasoningEffort", options.reasoningEffort],
["dreaming", options.dreaming],
["resources", options.resources],
].find(([, value]) => value !== undefined);
if (persistedOption) {
throw new Error(
`stateless sessions cannot set ${persistedOption[0]} because it changes persisted agent configuration.`,
);
}
}
validateClientToolset(options.toolset);
validateSkillSources(options.skillSources);
validateMcpServers(options.mcpServers);
Expand Down
Loading