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

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 (49) hide show
  1. package/AGENTS.md +47 -0
  2. package/README.md +17 -0
  3. package/dist/client-entry.js +800 -732
  4. package/dist/client-entry.js.map +8 -5
  5. package/dist/cloud-sandbox.d.ts +33 -0
  6. package/dist/cloud-sandbox.d.ts.map +1 -0
  7. package/dist/cloud-session.d.ts.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +799 -732
  11. package/dist/index.js.map +9 -6
  12. package/dist/remote-client-session-core.d.ts +6 -93
  13. package/dist/remote-client-session-core.d.ts.map +1 -1
  14. package/dist/remote-session-protocol.d.ts +130 -0
  15. package/dist/remote-session-protocol.d.ts.map +1 -0
  16. package/dist/remote-turn-coordinator.d.ts +49 -0
  17. package/dist/remote-turn-coordinator.d.ts.map +1 -0
  18. package/dist/types.d.ts +2 -20
  19. package/dist/types.d.ts.map +1 -1
  20. package/package.json +5 -2
  21. package/src/app-server-management.ts +641 -0
  22. package/src/app-server-session.ts +948 -0
  23. package/src/cli-resolver.ts +46 -0
  24. package/src/client-base.ts +482 -0
  25. package/src/client-entry.ts +31 -0
  26. package/src/client.ts +138 -0
  27. package/src/cloud-management.ts +360 -0
  28. package/src/cloud-sandbox.ts +117 -0
  29. package/src/cloud-session.ts +1313 -0
  30. package/src/index.ts +440 -0
  31. package/src/interactiveToolPolicy.ts +62 -0
  32. package/src/local-app-server-session.ts +39 -0
  33. package/src/local-app-server.ts +137 -0
  34. package/src/management-types.ts +133 -0
  35. package/src/management.ts +206 -0
  36. package/src/protocol.ts +249 -0
  37. package/src/remote-client-session-core.ts +786 -0
  38. package/src/remote-session-protocol.ts +660 -0
  39. package/src/remote-turn-coordinator.ts +505 -0
  40. package/src/remote.ts +177 -0
  41. package/src/repositories.ts +340 -0
  42. package/src/request-ids.ts +33 -0
  43. package/src/session.ts +1638 -0
  44. package/src/stream-events.ts +88 -0
  45. package/src/tool-helpers.ts +147 -0
  46. package/src/transport.ts +484 -0
  47. package/src/types.ts +1328 -0
  48. package/src/validation.ts +223 -0
  49. package/src/websocket.ts +22 -0
@@ -0,0 +1,360 @@
1
+ import type {
2
+ ManagementQuery,
3
+ ManagementTransport,
4
+ } from "./management.js";
5
+ import type {
6
+ ConversationMessagesResult,
7
+ LettaAgent,
8
+ LettaConversation,
9
+ } from "./management-types.js";
10
+ import type {
11
+ LettaCodeCloudClientOptions,
12
+ LettaCodeModelEntry,
13
+ ListModelsResult,
14
+ } from "./types.js";
15
+
16
+ const DEFAULT_CLOUD_API_BASE_URL = "https://api.letta.com";
17
+
18
+ function defaultApiKey(): string | undefined {
19
+ const env = (
20
+ globalThis as {
21
+ process?: { env?: Record<string, string | undefined> };
22
+ }
23
+ ).process?.env;
24
+ return env?.LETTA_API_KEY ?? env?.LETTA_CLOUD_API_KEY;
25
+ }
26
+
27
+ function bearerToken(
28
+ headers: Record<string, string> | undefined,
29
+ ): string | undefined {
30
+ const authorization = headers?.Authorization ?? headers?.authorization;
31
+ return authorization?.match(/^Bearer\s+(.+)$/i)?.[1];
32
+ }
33
+
34
+ function apiBaseUrl(value: string | undefined): string {
35
+ const url = new URL(value ?? DEFAULT_CLOUD_API_BASE_URL);
36
+ url.pathname = url.pathname.replace(/\/+$/, "");
37
+ url.search = "";
38
+ url.hash = "";
39
+ return url.toString().replace(/\/$/, "");
40
+ }
41
+
42
+ function requestHeaders(
43
+ options: LettaCodeCloudClientOptions,
44
+ ): Record<string, string> {
45
+ const headers: Record<string, string> = {
46
+ "Content-Type": "application/json",
47
+ ...(options.headers ?? {}),
48
+ };
49
+ const apiKey =
50
+ options.apiKey ?? bearerToken(options.headers) ?? defaultApiKey();
51
+ if (apiKey && !headers.Authorization && !headers.authorization) {
52
+ headers.Authorization = `Bearer ${apiKey}`;
53
+ }
54
+ return headers;
55
+ }
56
+
57
+ function appendQuery(url: URL, query: ManagementQuery): void {
58
+ for (const [key, value] of Object.entries(query)) {
59
+ if (value === undefined || value === null) continue;
60
+ if (Array.isArray(value)) {
61
+ for (const item of value) url.searchParams.append(key, item);
62
+ } else {
63
+ url.searchParams.set(key, String(value));
64
+ }
65
+ }
66
+ }
67
+
68
+ async function parseResponse(response: Response): Promise<unknown> {
69
+ const text = await response.text();
70
+ if (!text) return null;
71
+ try {
72
+ return JSON.parse(text) as unknown;
73
+ } catch {
74
+ return text;
75
+ }
76
+ }
77
+
78
+ function responseErrorMessage(body: unknown, fallback: string): string {
79
+ if (body && typeof body === "object") {
80
+ const record = body as Record<string, unknown>;
81
+ const message = record.message ?? record.error ?? record.detail;
82
+ if (typeof message === "string" && message.length > 0) return message;
83
+ }
84
+ return fallback;
85
+ }
86
+
87
+ function cloudRequestError(
88
+ response: Response,
89
+ body: unknown,
90
+ action: string,
91
+ url: URL,
92
+ options: LettaCodeCloudClientOptions,
93
+ ): Error {
94
+ const parts = [
95
+ `${action} failed`,
96
+ responseErrorMessage(body, `HTTP ${response.status}`),
97
+ `URL: ${url.toString()}`,
98
+ ];
99
+ if (response.status === 401 || response.status === 403) {
100
+ const hasApiKey = Boolean(
101
+ options.apiKey ??
102
+ bearerToken(options.headers) ??
103
+ defaultApiKey(),
104
+ );
105
+ parts.push(
106
+ hasApiKey
107
+ ? "Authentication failed — the API key may be invalid or lack permissions for this resource."
108
+ : "No API key found. Set LETTA_API_KEY (or pass apiKey in client options).",
109
+ );
110
+ }
111
+ return new Error(parts.join(" — "));
112
+ }
113
+
114
+ function asObject<T>(body: unknown, action: string): T {
115
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
116
+ throw new Error(`${action} response did not include an object.`);
117
+ }
118
+ return body as T;
119
+ }
120
+
121
+ function asArray<T>(body: unknown, action: string): T[] {
122
+ if (!Array.isArray(body)) {
123
+ throw new Error(`${action} response did not include an array.`);
124
+ }
125
+ return body as T[];
126
+ }
127
+
128
+ function cloudModelEntry(
129
+ raw: Record<string, unknown>,
130
+ ): LettaCodeModelEntry | null {
131
+ if (typeof raw.handle !== "string" || raw.handle.length === 0) {
132
+ return null;
133
+ }
134
+ const label =
135
+ typeof raw.display_name === "string"
136
+ ? raw.display_name
137
+ : typeof raw.name === "string"
138
+ ? raw.name
139
+ : raw.handle;
140
+ return {
141
+ ...raw,
142
+ id:
143
+ typeof raw.id === "string" && raw.id.length > 0
144
+ ? raw.id
145
+ : raw.handle,
146
+ handle: raw.handle,
147
+ label,
148
+ description:
149
+ typeof raw.description === "string" ? raw.description : "",
150
+ };
151
+ }
152
+
153
+ export class CloudManagementTransport implements ManagementTransport {
154
+ constructor(private readonly options: LettaCodeCloudClientOptions) {}
155
+
156
+ listAgents(query: ManagementQuery): Promise<LettaAgent[]> {
157
+ return this.getArray("/v1/agents/", query, "Cloud list agents");
158
+ }
159
+
160
+ retrieveAgent(agentId: string): Promise<LettaAgent> {
161
+ return this.getObject(
162
+ `/v1/agents/${encodeURIComponent(agentId)}`,
163
+ {},
164
+ "Cloud retrieve agent",
165
+ );
166
+ }
167
+
168
+ updateAgent(
169
+ agentId: string,
170
+ body: Record<string, unknown>,
171
+ ): Promise<LettaAgent> {
172
+ return this.requestObject(
173
+ `/v1/agents/${encodeURIComponent(agentId)}`,
174
+ "PATCH",
175
+ body,
176
+ "Cloud update agent",
177
+ );
178
+ }
179
+
180
+ async deleteAgent(agentId: string): Promise<void> {
181
+ // No trailing slash: the production api.letta.com router 404s
182
+ // trailing-slash non-GET agent routes (see `POST /v1/agents` in
183
+ // cloud-session.ts — GET tolerates the slash; DELETE does not).
184
+ await this.requestUrl(
185
+ this.url(`/v1/agents/${encodeURIComponent(agentId)}`),
186
+ "DELETE",
187
+ undefined,
188
+ "Cloud delete agent",
189
+ );
190
+ }
191
+
192
+ async listModels(): Promise<ListModelsResult> {
193
+ // No trailing slash for consistency with the non-GET routes above (the
194
+ // production router only tolerates trailing slashes on GET).
195
+ const entries = await this.getArray<Record<string, unknown>>(
196
+ "/v1/models",
197
+ {},
198
+ "Cloud list models",
199
+ );
200
+ const normalized = entries.flatMap((entry) => {
201
+ const model = cloudModelEntry(entry);
202
+ return model ? [model] : [];
203
+ });
204
+ return {
205
+ entries: normalized,
206
+ // Cloud's authenticated endpoint already returns the models available to
207
+ // this user, so its catalog is also the authoritative availability set.
208
+ availableHandles: normalized.map((entry) => entry.handle),
209
+ };
210
+ }
211
+
212
+ listConversations(
213
+ query: ManagementQuery,
214
+ ): Promise<LettaConversation[]> {
215
+ return this.getArray(
216
+ "/v1/conversations/",
217
+ query,
218
+ "Cloud list conversations",
219
+ );
220
+ }
221
+
222
+ retrieveConversation(
223
+ conversationId: string,
224
+ ): Promise<LettaConversation> {
225
+ return this.getObject(
226
+ `/v1/conversations/${encodeURIComponent(conversationId)}`,
227
+ {},
228
+ "Cloud retrieve conversation",
229
+ );
230
+ }
231
+
232
+ createConversation(
233
+ body: Record<string, unknown>,
234
+ ): Promise<LettaConversation> {
235
+ const { agent_id: agentId, ...requestBody } = body;
236
+ if (typeof agentId !== "string" || agentId.length === 0) {
237
+ throw new Error("createConversation() requires a non-empty agentId.");
238
+ }
239
+ const url = this.url("/v1/conversations/");
240
+ url.searchParams.set("agent_id", agentId);
241
+ return this.requestUrlObject(
242
+ url,
243
+ "POST",
244
+ requestBody,
245
+ "Cloud create conversation",
246
+ );
247
+ }
248
+
249
+ updateConversation(
250
+ conversationId: string,
251
+ body: Record<string, unknown>,
252
+ ): Promise<LettaConversation> {
253
+ return this.requestObject(
254
+ `/v1/conversations/${encodeURIComponent(conversationId)}`,
255
+ "PATCH",
256
+ body,
257
+ "Cloud update conversation",
258
+ );
259
+ }
260
+
261
+ async listConversationMessages(
262
+ conversationId: string,
263
+ query: ManagementQuery,
264
+ ): Promise<ConversationMessagesResult> {
265
+ const messages = await this.getArray<Record<string, unknown>>(
266
+ `/v1/conversations/${encodeURIComponent(conversationId)}/messages`,
267
+ query,
268
+ "Cloud list conversation messages",
269
+ );
270
+ return { messages };
271
+ }
272
+
273
+ private async getArray<T>(
274
+ path: string,
275
+ query: ManagementQuery,
276
+ action: string,
277
+ ): Promise<T[]> {
278
+ const body = await this.get(path, query, action);
279
+ return asArray<T>(body, action);
280
+ }
281
+
282
+ private async getObject<T>(
283
+ path: string,
284
+ query: ManagementQuery,
285
+ action: string,
286
+ ): Promise<T> {
287
+ const body = await this.get(path, query, action);
288
+ return asObject<T>(body, action);
289
+ }
290
+
291
+ private get(
292
+ path: string,
293
+ query: ManagementQuery,
294
+ action: string,
295
+ ): Promise<unknown> {
296
+ const url = this.url(path);
297
+ appendQuery(url, query);
298
+ return this.requestUrl(url, "GET", undefined, action);
299
+ }
300
+
301
+ private requestObject<T>(
302
+ path: string,
303
+ method: string,
304
+ body: Record<string, unknown>,
305
+ action: string,
306
+ ): Promise<T> {
307
+ return this.requestUrlObject(
308
+ this.url(path),
309
+ method,
310
+ body,
311
+ action,
312
+ );
313
+ }
314
+
315
+ private async requestUrlObject<T>(
316
+ url: URL,
317
+ method: string,
318
+ body: Record<string, unknown>,
319
+ action: string,
320
+ ): Promise<T> {
321
+ return asObject<T>(
322
+ await this.requestUrl(url, method, body, action),
323
+ action,
324
+ );
325
+ }
326
+
327
+ private async requestUrl(
328
+ url: URL,
329
+ method: string,
330
+ body: Record<string, unknown> | undefined,
331
+ action: string,
332
+ ): Promise<unknown> {
333
+ const fetchImpl = (this.options.fetch ?? globalThis.fetch)?.bind(
334
+ globalThis,
335
+ );
336
+ if (!fetchImpl) {
337
+ throw new Error("No fetch implementation available for cloud backend.");
338
+ }
339
+ const response = await fetchImpl(url, {
340
+ method,
341
+ headers: requestHeaders(this.options),
342
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
343
+ });
344
+ const responseBody = await parseResponse(response);
345
+ if (!response.ok) {
346
+ throw cloudRequestError(
347
+ response,
348
+ responseBody,
349
+ action,
350
+ url,
351
+ this.options,
352
+ );
353
+ }
354
+ return responseBody;
355
+ }
356
+
357
+ private url(path: string): URL {
358
+ return new URL(`${apiBaseUrl(this.options.apiBaseUrl)}${path}`);
359
+ }
360
+ }
@@ -0,0 +1,117 @@
1
+ /** A GitHub repository cloned into an SDK-managed Cloud sandbox. */
2
+ export interface GitHubRepositoryRef {
3
+ owner: string;
4
+ repo: string;
5
+ }
6
+
7
+ export interface LettaCodeCloudSandboxOptions {
8
+ /**
9
+ * TTL to request when refreshing an SDK-managed sandbox. Defaults to 5
10
+ * minutes, matching the Cloud API default. Valid range: 1-60.
11
+ */
12
+ ttlMinutes?: number;
13
+ /** Timeout waiting for a newly created sandbox environment to come online. */
14
+ readyTimeoutMs?: number;
15
+ /** Poll interval while waiting for a newly created sandbox environment. */
16
+ readyPollIntervalMs?: number;
17
+ /** Interval for proactive TTL refreshes while the session is open. */
18
+ refreshIntervalMs?: number;
19
+ /**
20
+ * GitHub repositories to clone into `/root/workspace` when creating the
21
+ * managed sandbox. Up to 10 repositories may be provided. Private
22
+ * repositories require access through the organization's GitHub integration.
23
+ */
24
+ githubRepositories?: GitHubRepositoryRef[];
25
+ /**
26
+ * Best-effort terminate the SDK-managed sandbox on session close. Defaults to
27
+ * false so other sessions for the same conversation and reconnecting clients
28
+ * can continue using it. Set true to restore eager cleanup when this session
29
+ * exclusively owns the sandbox.
30
+ */
31
+ terminateOnClose?: boolean;
32
+ }
33
+
34
+ const MIN_TTL_MINUTES = 1;
35
+ const MAX_TTL_MINUTES = 60;
36
+ const MAX_GITHUB_REPOSITORIES = 10;
37
+ const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9-]+$/;
38
+ const GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]+$/;
39
+
40
+ function validatePositiveInteger(value: number | undefined, name: string): void {
41
+ if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
42
+ throw new Error(`Invalid ${name}. Expected a positive integer.`);
43
+ }
44
+ }
45
+
46
+ export function validateCloudSandboxOptions(
47
+ options: LettaCodeCloudSandboxOptions | undefined,
48
+ name: string,
49
+ ): void {
50
+ if (options === undefined) return;
51
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
52
+ throw new Error(`Invalid ${name}. Expected an object.`);
53
+ }
54
+ if (
55
+ options.ttlMinutes !== undefined &&
56
+ (!Number.isInteger(options.ttlMinutes) ||
57
+ options.ttlMinutes < MIN_TTL_MINUTES ||
58
+ options.ttlMinutes > MAX_TTL_MINUTES)
59
+ ) {
60
+ throw new Error(
61
+ `Invalid ${name}.ttlMinutes. Expected an integer between ${MIN_TTL_MINUTES} and ${MAX_TTL_MINUTES}.`,
62
+ );
63
+ }
64
+ validatePositiveInteger(options.readyTimeoutMs, `${name}.readyTimeoutMs`);
65
+ validatePositiveInteger(
66
+ options.readyPollIntervalMs,
67
+ `${name}.readyPollIntervalMs`,
68
+ );
69
+ validatePositiveInteger(options.refreshIntervalMs, `${name}.refreshIntervalMs`);
70
+
71
+ if (options.githubRepositories !== undefined) {
72
+ if (!Array.isArray(options.githubRepositories)) {
73
+ throw new Error(
74
+ `Invalid ${name}.githubRepositories. Expected an array.`,
75
+ );
76
+ }
77
+ if (options.githubRepositories.length > MAX_GITHUB_REPOSITORIES) {
78
+ throw new Error(
79
+ `Invalid ${name}.githubRepositories. Expected at most ${MAX_GITHUB_REPOSITORIES} repositories.`,
80
+ );
81
+ }
82
+ for (const [index, repository] of options.githubRepositories.entries()) {
83
+ if (
84
+ repository === null ||
85
+ typeof repository !== "object" ||
86
+ Array.isArray(repository)
87
+ ) {
88
+ throw new Error(
89
+ `Invalid ${name}.githubRepositories[${index}]. Expected an object.`,
90
+ );
91
+ }
92
+ if (
93
+ typeof repository.owner !== "string" ||
94
+ !GITHUB_OWNER_PATTERN.test(repository.owner)
95
+ ) {
96
+ throw new Error(
97
+ `Invalid ${name}.githubRepositories[${index}].owner.`,
98
+ );
99
+ }
100
+ if (
101
+ typeof repository.repo !== "string" ||
102
+ !GITHUB_REPOSITORY_PATTERN.test(repository.repo)
103
+ ) {
104
+ throw new Error(
105
+ `Invalid ${name}.githubRepositories[${index}].repo.`,
106
+ );
107
+ }
108
+ }
109
+ }
110
+
111
+ if (
112
+ options.terminateOnClose !== undefined &&
113
+ typeof options.terminateOnClose !== "boolean"
114
+ ) {
115
+ throw new Error(`Invalid ${name}.terminateOnClose. Expected a boolean.`);
116
+ }
117
+ }