@letta-ai/letta-agent-sdk 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/AGENTS.md +44 -0
  2. package/README.md +29 -0
  3. package/dist/app-server-management.d.ts +3 -48
  4. package/dist/app-server-management.d.ts.map +1 -1
  5. package/dist/app-server-session.d.ts +0 -6
  6. package/dist/app-server-session.d.ts.map +1 -1
  7. package/dist/client-base.d.ts +7 -15
  8. package/dist/client-base.d.ts.map +1 -1
  9. package/dist/client-entry.js +979 -1010
  10. package/dist/client-entry.js.map +15 -11
  11. package/dist/client.d.ts +3 -3
  12. package/dist/client.d.ts.map +1 -1
  13. package/dist/cloud-sandbox.d.ts +33 -0
  14. package/dist/cloud-sandbox.d.ts.map +1 -0
  15. package/dist/cloud-session.d.ts.map +1 -1
  16. package/dist/cloud-status-transport.d.ts +53 -0
  17. package/dist/cloud-status-transport.d.ts.map +1 -0
  18. package/dist/index.d.ts +7 -16
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3242 -4298
  21. package/dist/index.js.map +20 -17
  22. package/dist/local-app-server-session.d.ts +1 -1
  23. package/dist/local-app-server-session.d.ts.map +1 -1
  24. package/dist/local-app-server.d.ts +11 -0
  25. package/dist/local-app-server.d.ts.map +1 -1
  26. package/dist/management.d.ts +0 -7
  27. package/dist/management.d.ts.map +1 -1
  28. package/dist/remote-client-session-core.d.ts +6 -93
  29. package/dist/remote-client-session-core.d.ts.map +1 -1
  30. package/dist/remote-session-protocol.d.ts +132 -0
  31. package/dist/remote-session-protocol.d.ts.map +1 -0
  32. package/dist/remote-turn-coordinator.d.ts +49 -0
  33. package/dist/remote-turn-coordinator.d.ts.map +1 -0
  34. package/dist/types.d.ts +59 -86
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/validation.d.ts.map +1 -1
  37. package/package.json +6 -3
  38. package/src/app-server-management.ts +405 -0
  39. package/src/app-server-session.ts +916 -0
  40. package/src/cli-resolver.ts +46 -0
  41. package/src/client-base.ts +456 -0
  42. package/src/client-entry.ts +31 -0
  43. package/src/client.ts +99 -0
  44. package/src/cloud-management.ts +360 -0
  45. package/src/cloud-sandbox.ts +117 -0
  46. package/src/cloud-session.ts +1055 -0
  47. package/src/cloud-status-transport.ts +305 -0
  48. package/src/index.ts +422 -0
  49. package/src/interactiveToolPolicy.ts +62 -0
  50. package/src/local-app-server-session.ts +49 -0
  51. package/src/local-app-server.ts +231 -0
  52. package/src/management-types.ts +133 -0
  53. package/src/management.ts +199 -0
  54. package/src/remote-client-session-core.ts +786 -0
  55. package/src/remote-session-protocol.ts +674 -0
  56. package/src/remote-turn-coordinator.ts +523 -0
  57. package/src/remote.ts +177 -0
  58. package/src/repositories.ts +340 -0
  59. package/src/request-ids.ts +33 -0
  60. package/src/stream-events.ts +88 -0
  61. package/src/tool-helpers.ts +147 -0
  62. package/src/types.ts +1281 -0
  63. package/src/validation.ts +238 -0
  64. package/src/websocket.ts +22 -0
  65. package/dist/protocol.d.ts +0 -205
  66. package/dist/protocol.d.ts.map +0 -1
  67. package/dist/session.d.ts +0 -155
  68. package/dist/session.d.ts.map +0 -1
  69. package/dist/transport.d.ts +0 -53
  70. package/dist/transport.d.ts.map +0 -1
@@ -0,0 +1,231 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ createMemoryConfinementLauncher,
6
+ type MemoryConfinementLauncherInput,
7
+ type MemoryConfinementLauncherResult,
8
+ } from "@letta-ai/letta-code/memory-confinement";
9
+ import { findLettaCli } from "./cli-resolver.js";
10
+
11
+ export interface LocalAppServerHandle {
12
+ url: string;
13
+ close(): void;
14
+ }
15
+
16
+ export interface StartLocalAppServerOptions {
17
+ listen?: string;
18
+ backend?: string;
19
+ startupTimeoutMs?: number;
20
+ cliPath?: string;
21
+ env?: Record<string, string | undefined>;
22
+ filesystemConfinement?: "memory";
23
+ agentId?: string;
24
+ }
25
+
26
+ interface LocalAppServerProcess {
27
+ command: string;
28
+ args: string[];
29
+ env: NodeJS.ProcessEnv;
30
+ }
31
+
32
+ type MemoryConfinementLauncher = (
33
+ input: MemoryConfinementLauncherInput,
34
+ ) => MemoryConfinementLauncherResult;
35
+
36
+ const DEFAULT_LISTEN_URL = "ws://127.0.0.1:0";
37
+ const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
38
+ const LISTENING_RE = /^Listening on\s+(ws:\/\/\S+)\s*$/m;
39
+
40
+ function targetHomeDirectory(env: NodeJS.ProcessEnv): string {
41
+ if (process.platform === "win32") {
42
+ const profile = env.USERPROFILE?.trim();
43
+ if (profile) return profile;
44
+ const drive = env.HOMEDRIVE?.trim();
45
+ const path = env.HOMEPATH?.trim();
46
+ if (drive && path) return `${drive}${path}`;
47
+ return homedir();
48
+ }
49
+ return env.HOME?.trim() || homedir();
50
+ }
51
+
52
+ function withDefaultMemoryDirectory(
53
+ env: NodeJS.ProcessEnv,
54
+ options: StartLocalAppServerOptions,
55
+ ): NodeJS.ProcessEnv {
56
+ const explicitMemoryDir = options.env?.MEMORY_DIR?.trim();
57
+ const explicitLettaMemoryDir = options.env?.LETTA_MEMORY_DIR?.trim();
58
+ const scopedEnv = { ...env };
59
+ if (explicitMemoryDir || explicitLettaMemoryDir) {
60
+ if (!explicitMemoryDir) delete scopedEnv.MEMORY_DIR;
61
+ if (!explicitLettaMemoryDir) delete scopedEnv.LETTA_MEMORY_DIR;
62
+ return scopedEnv;
63
+ }
64
+
65
+ // Never inherit the SDK process's agent scope into a different session.
66
+ delete scopedEnv.MEMORY_DIR;
67
+ delete scopedEnv.LETTA_MEMORY_DIR;
68
+ if (!options.agentId) return scopedEnv;
69
+
70
+ const homeDir = targetHomeDirectory(env);
71
+ // Mirrors Letta Code's getScopedMemoryFilesystemRoot contract. The SDK pins
72
+ // an exact Letta Code version so the launch policy and layout move together.
73
+ const memoryDir =
74
+ options.backend === "api"
75
+ ? join(homeDir, ".letta", "agents", options.agentId, "memory")
76
+ : join(
77
+ env.LETTA_LOCAL_BACKEND_DIR?.trim() ||
78
+ join(homeDir, ".letta", "lc-local-backend"),
79
+ "memfs",
80
+ options.agentId,
81
+ "memory",
82
+ );
83
+ return { ...scopedEnv, MEMORY_DIR: memoryDir };
84
+ }
85
+
86
+ function appendLine(buffer: string, chunk: unknown): string {
87
+ return buffer + String(chunk);
88
+ }
89
+
90
+ function tryExtractListeningUrl(output: string): string | null {
91
+ const match = output.match(LISTENING_RE);
92
+ return match?.[1] ?? null;
93
+ }
94
+
95
+ export function buildLocalAppServerArgs(
96
+ cliPath: string,
97
+ options: Pick<StartLocalAppServerOptions, "backend" | "listen"> = {},
98
+ ): string[] {
99
+ return [
100
+ cliPath,
101
+ ...(options.backend !== undefined ? ["--backend", options.backend] : []),
102
+ "app-server",
103
+ "--listen",
104
+ options.listen ?? DEFAULT_LISTEN_URL,
105
+ ];
106
+ }
107
+
108
+ export function buildLocalAppServerProcess(
109
+ cliPath: string,
110
+ options: StartLocalAppServerOptions = {},
111
+ confineMemory: MemoryConfinementLauncher = createMemoryConfinementLauncher,
112
+ ): LocalAppServerProcess {
113
+ const env = { ...process.env, ...(options.env ?? {}) };
114
+ const launcher = [
115
+ process.execPath,
116
+ ...buildLocalAppServerArgs(cliPath, options),
117
+ ];
118
+ if (options.filesystemConfinement !== "memory") {
119
+ return {
120
+ command: launcher[0] as string,
121
+ args: launcher.slice(1),
122
+ env,
123
+ };
124
+ }
125
+
126
+ const confined = confineMemory({
127
+ launcher,
128
+ env: withDefaultMemoryDirectory(env, options),
129
+ });
130
+ return {
131
+ command: confined.launcher[0] as string,
132
+ args: confined.launcher.slice(1),
133
+ env: confined.env,
134
+ };
135
+ }
136
+
137
+ function terminateProcess(child: ChildProcess): void {
138
+ if (child.exitCode !== null || child.signalCode !== null) return;
139
+ child.kill("SIGTERM");
140
+ setTimeout(() => {
141
+ if (child.exitCode === null && child.signalCode === null) {
142
+ child.kill("SIGKILL");
143
+ }
144
+ }, 1_000).unref?.();
145
+ }
146
+
147
+ /**
148
+ * Spawn an SDK-owned Letta Code app-server on an ephemeral loopback port.
149
+ */
150
+ export function startLocalAppServer(
151
+ options: StartLocalAppServerOptions = {},
152
+ ): Promise<LocalAppServerHandle> {
153
+ const cliPath = options.cliPath ?? findLettaCli();
154
+ const processSpec = buildLocalAppServerProcess(cliPath, options);
155
+ const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
156
+
157
+ return new Promise((resolve, reject) => {
158
+ const child = spawn(processSpec.command, processSpec.args, {
159
+ stdio: ["ignore", "pipe", "pipe"],
160
+ env: processSpec.env,
161
+ });
162
+
163
+ let settled = false;
164
+ let output = "";
165
+
166
+ const cleanup = () => {
167
+ child.stdout?.off("data", onStdout);
168
+ child.stderr?.off("data", onStderr);
169
+ child.off("error", onError);
170
+ child.off("exit", onExit);
171
+ clearTimeout(timeout);
172
+ };
173
+
174
+ const fail = (error: Error) => {
175
+ if (settled) return;
176
+ settled = true;
177
+ cleanup();
178
+ terminateProcess(child);
179
+ reject(error);
180
+ };
181
+
182
+ const succeed = (url: string) => {
183
+ if (settled) return;
184
+ settled = true;
185
+ cleanup();
186
+ resolve({
187
+ url,
188
+ close: () => terminateProcess(child),
189
+ });
190
+ };
191
+
192
+ const onOutput = (chunk: unknown) => {
193
+ output = appendLine(output, chunk);
194
+ const url = tryExtractListeningUrl(output);
195
+ if (url) succeed(url);
196
+ };
197
+
198
+ const onStdout = (chunk: unknown) => onOutput(chunk);
199
+ const onStderr = (chunk: unknown) => {
200
+ // Startup failures are printed to stderr by the CLI. Keep stderr in the
201
+ // collected output so timeout/exit errors are actionable.
202
+ output = appendLine(output, chunk);
203
+ };
204
+ const onError = (error: Error) => fail(error);
205
+ const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
206
+ if (settled) return;
207
+ fail(
208
+ new Error(
209
+ `Local Letta Code app-server exited before listening (code=${code ?? "null"}, signal=${signal ?? "null"}).${
210
+ output ? ` Output:\n${output.trim()}` : ""
211
+ }`,
212
+ ),
213
+ );
214
+ };
215
+
216
+ const timeout = setTimeout(() => {
217
+ fail(
218
+ new Error(
219
+ `Timed out waiting for local Letta Code app-server to start.${
220
+ output ? ` Output:\n${output.trim()}` : ""
221
+ }`,
222
+ ),
223
+ );
224
+ }, startupTimeoutMs);
225
+
226
+ child.stdout?.on("data", onStdout);
227
+ child.stderr?.on("data", onStderr);
228
+ child.once("error", onError);
229
+ child.once("exit", onExit);
230
+ });
231
+ }
@@ -0,0 +1,133 @@
1
+ import type { ListMessagesResult, ListModelsResult } from "./types.js";
2
+
3
+ /** Agent state returned by either the Cloud API or Letta Code app-server. */
4
+ export type LettaAgent = Record<string, unknown> & {
5
+ id: string;
6
+ name: string;
7
+ description?: string | null;
8
+ model?: string | null;
9
+ model_settings?: Record<string, unknown> | null;
10
+ tags?: string[];
11
+ created_at?: string | null;
12
+ updated_at?: string | null;
13
+ };
14
+
15
+ /** Conversation state returned by either the Cloud API or Letta Code app-server. */
16
+ export type LettaConversation = Record<string, unknown> & {
17
+ id: string;
18
+ agent_id: string;
19
+ summary?: string | null;
20
+ description?: string | null;
21
+ model?: string | null;
22
+ model_settings?: Record<string, unknown> | null;
23
+ archived?: boolean;
24
+ created_at?: string | null;
25
+ updated_at?: string | null;
26
+ last_message_at?: string | null;
27
+ };
28
+
29
+ /** Raw Letta API message returned from conversation history. */
30
+ export type LettaConversationMessage = Record<string, unknown>;
31
+
32
+ export interface ListAgentsOptions {
33
+ before?: string;
34
+ after?: string;
35
+ limit?: number;
36
+ order?: "asc" | "desc";
37
+ orderBy?: "createdAt" | "lastRunCompletion";
38
+ /** Search agent names. */
39
+ query?: string;
40
+ /** Match one exact agent name. */
41
+ name?: string;
42
+ tags?: string[];
43
+ matchAllTags?: boolean;
44
+ /** Relationships to hydrate in each returned agent. */
45
+ include?: string[];
46
+ }
47
+
48
+ export interface UpdateAgentOptions {
49
+ name?: string | null;
50
+ description?: string | null;
51
+ model?: string | null;
52
+ modelSettings?: Record<string, unknown> | null;
53
+ system?: string | null;
54
+ tags?: string[] | null;
55
+ hidden?: boolean | null;
56
+ contextWindowLimit?: number | null;
57
+ }
58
+
59
+ export interface ListConversationsOptions {
60
+ agentId?: string;
61
+ after?: string;
62
+ limit?: number;
63
+ order?: "asc" | "desc";
64
+ orderBy?: "createdAt" | "lastRunCompletion" | "lastMessageAt";
65
+ archiveStatus?: "unarchived" | "archived" | "all";
66
+ summarySearch?: string;
67
+ }
68
+
69
+ export interface CreateConversationOptions {
70
+ agentId: string;
71
+ summary?: string | null;
72
+ description?: string | null;
73
+ model?: string | null;
74
+ modelSettings?: Record<string, unknown> | null;
75
+ contextWindowLimit?: number | null;
76
+ hidden?: boolean;
77
+ }
78
+
79
+ export interface UpdateConversationOptions {
80
+ summary?: string | null;
81
+ description?: string | null;
82
+ model?: string | null;
83
+ modelSettings?: Record<string, unknown> | null;
84
+ contextWindowLimit?: number | null;
85
+ archived?: boolean | null;
86
+ }
87
+
88
+ export interface ConversationMessagesOptions {
89
+ before?: string;
90
+ after?: string;
91
+ order?: "asc" | "desc";
92
+ limit?: number;
93
+ }
94
+
95
+ export type ConversationMessagesResult = ListMessagesResult & {
96
+ messages: LettaConversationMessage[];
97
+ };
98
+
99
+ export interface AgentsClient {
100
+ list(options?: ListAgentsOptions): Promise<LettaAgent[]>;
101
+ retrieve(agentId: string): Promise<LettaAgent>;
102
+ update(
103
+ agentId: string,
104
+ options: UpdateAgentOptions,
105
+ ): Promise<LettaAgent>;
106
+ delete(agentId: string): Promise<void>;
107
+ }
108
+
109
+ export interface ModelsClient {
110
+ /**
111
+ * List the model catalog without opening a session.
112
+ *
113
+ * Uses the same normalized result shape as `session.listModels()`, including
114
+ * availability and BYOK-alias metadata when the backend provides it.
115
+ */
116
+ list(): Promise<ListModelsResult>;
117
+ }
118
+
119
+ export interface ConversationsClient {
120
+ list(
121
+ options?: ListConversationsOptions,
122
+ ): Promise<LettaConversation[]>;
123
+ retrieve(conversationId: string): Promise<LettaConversation>;
124
+ create(options: CreateConversationOptions): Promise<LettaConversation>;
125
+ update(
126
+ conversationId: string,
127
+ options: UpdateConversationOptions,
128
+ ): Promise<LettaConversation>;
129
+ listMessages(
130
+ conversationId: string,
131
+ options?: ConversationMessagesOptions,
132
+ ): Promise<ConversationMessagesResult>;
133
+ }
@@ -0,0 +1,199 @@
1
+ import type {
2
+ AgentsClient,
3
+ ConversationsClient,
4
+ ConversationMessagesResult,
5
+ CreateConversationOptions,
6
+ LettaAgent,
7
+ LettaConversation,
8
+ ListAgentsOptions,
9
+ ListConversationsOptions,
10
+ ModelsClient,
11
+ UpdateAgentOptions,
12
+ UpdateConversationOptions,
13
+ ConversationMessagesOptions,
14
+ } from "./management-types.js";
15
+ import type { ListModelsResult } from "./types.js";
16
+
17
+ export type ManagementQuery = Record<
18
+ string,
19
+ string | number | boolean | string[] | null | undefined
20
+ >;
21
+
22
+ export interface ManagementTransport {
23
+ listAgents(query: ManagementQuery): Promise<LettaAgent[]>;
24
+ retrieveAgent(agentId: string): Promise<LettaAgent>;
25
+ updateAgent(
26
+ agentId: string,
27
+ body: Record<string, unknown>,
28
+ ): Promise<LettaAgent>;
29
+ deleteAgent(agentId: string): Promise<void>;
30
+ listModels(): Promise<ListModelsResult>;
31
+ listConversations(
32
+ query: ManagementQuery,
33
+ ): Promise<LettaConversation[]>;
34
+ retrieveConversation(
35
+ conversationId: string,
36
+ ): Promise<LettaConversation>;
37
+ createConversation(
38
+ body: Record<string, unknown>,
39
+ ): Promise<LettaConversation>;
40
+ updateConversation(
41
+ conversationId: string,
42
+ body: Record<string, unknown>,
43
+ ): Promise<LettaConversation>;
44
+ listConversationMessages(
45
+ conversationId: string,
46
+ query: ManagementQuery,
47
+ ): Promise<ConversationMessagesResult>;
48
+ }
49
+
50
+ type TransportProvider = () => ManagementTransport;
51
+
52
+ function definedEntries(
53
+ values: Record<string, unknown>,
54
+ ): Record<string, unknown> {
55
+ return Object.fromEntries(
56
+ Object.entries(values).filter(([, value]) => value !== undefined),
57
+ );
58
+ }
59
+
60
+ function assertNonEmptyId(value: string, name: string): void {
61
+ if (typeof value !== "string" || value.trim().length === 0) {
62
+ throw new Error(`Invalid ${name}. Expected a non-empty string.`);
63
+ }
64
+ }
65
+
66
+ function agentListQuery(options: ListAgentsOptions): ManagementQuery {
67
+ const orderBy = options.orderBy?.replace(
68
+ /[A-Z]/g,
69
+ (character) => `_${character.toLowerCase()}`,
70
+ );
71
+ return {
72
+ before: options.before,
73
+ after: options.after,
74
+ limit: options.limit,
75
+ order: options.order,
76
+ order_by: orderBy,
77
+ query_text: options.query,
78
+ name: options.name,
79
+ tags: options.tags,
80
+ match_all_tags: options.matchAllTags,
81
+ include: options.include,
82
+ };
83
+ }
84
+
85
+ function agentUpdateBody(options: UpdateAgentOptions): Record<string, unknown> {
86
+ return definedEntries({
87
+ name: options.name,
88
+ description: options.description,
89
+ model: options.model,
90
+ model_settings: options.modelSettings,
91
+ system: options.system,
92
+ tags: options.tags,
93
+ hidden: options.hidden,
94
+ context_window_limit: options.contextWindowLimit,
95
+ });
96
+ }
97
+
98
+ function conversationListQuery(
99
+ options: ListConversationsOptions,
100
+ ): ManagementQuery {
101
+ const orderBy = options.orderBy?.replace(
102
+ /[A-Z]/g,
103
+ (character) => `_${character.toLowerCase()}`,
104
+ );
105
+ return {
106
+ agent_id: options.agentId,
107
+ after: options.after,
108
+ limit: options.limit,
109
+ order: options.order,
110
+ order_by: orderBy,
111
+ archive_status: options.archiveStatus,
112
+ summary_search: options.summarySearch,
113
+ };
114
+ }
115
+
116
+ function conversationCreateBody(
117
+ options: CreateConversationOptions,
118
+ ): Record<string, unknown> {
119
+ return definedEntries({
120
+ agent_id: options.agentId,
121
+ summary: options.summary,
122
+ description: options.description,
123
+ model: options.model,
124
+ model_settings: options.modelSettings,
125
+ context_window_limit: options.contextWindowLimit,
126
+ hidden: options.hidden,
127
+ });
128
+ }
129
+
130
+ function conversationUpdateBody(
131
+ options: UpdateConversationOptions,
132
+ ): Record<string, unknown> {
133
+ return definedEntries({
134
+ summary: options.summary,
135
+ description: options.description,
136
+ model: options.model,
137
+ model_settings: options.modelSettings,
138
+ context_window_limit: options.contextWindowLimit,
139
+ archived: options.archived,
140
+ });
141
+ }
142
+
143
+ function conversationMessagesQuery(
144
+ options: ConversationMessagesOptions,
145
+ ): ManagementQuery {
146
+ return {
147
+ before: options.before,
148
+ after: options.after,
149
+ order: options.order,
150
+ limit: options.limit,
151
+ };
152
+ }
153
+
154
+ export function createAgentsClient(
155
+ transport: TransportProvider,
156
+ ): AgentsClient {
157
+ return {
158
+ list: (options = {}) =>
159
+ transport().listAgents(agentListQuery(options)),
160
+ retrieve: (agentId) => transport().retrieveAgent(agentId),
161
+ update: (agentId, options) =>
162
+ transport().updateAgent(agentId, agentUpdateBody(options)),
163
+ delete: async (agentId) => {
164
+ assertNonEmptyId(agentId, "agent id");
165
+ await transport().deleteAgent(agentId);
166
+ },
167
+ };
168
+ }
169
+
170
+ export function createModelsClient(
171
+ transport: TransportProvider,
172
+ ): ModelsClient {
173
+ return {
174
+ list: () => transport().listModels(),
175
+ };
176
+ }
177
+
178
+ export function createConversationsClient(
179
+ transport: TransportProvider,
180
+ ): ConversationsClient {
181
+ return {
182
+ list: (options = {}) =>
183
+ transport().listConversations(conversationListQuery(options)),
184
+ retrieve: (conversationId) =>
185
+ transport().retrieveConversation(conversationId),
186
+ create: (options) =>
187
+ transport().createConversation(conversationCreateBody(options)),
188
+ update: (conversationId, options) =>
189
+ transport().updateConversation(
190
+ conversationId,
191
+ conversationUpdateBody(options),
192
+ ),
193
+ listMessages: (conversationId, options = {}) =>
194
+ transport().listConversationMessages(
195
+ conversationId,
196
+ conversationMessagesQuery(options),
197
+ ),
198
+ };
199
+ }