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

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 (52) hide show
  1. package/AGENTS.md +35 -0
  2. package/README.md +22 -10
  3. package/dist/agent-repositories.d.ts +15 -0
  4. package/dist/agent-repositories.d.ts.map +1 -0
  5. package/dist/app-server-management.d.ts +10 -7
  6. package/dist/app-server-management.d.ts.map +1 -1
  7. package/dist/app-server-models.d.ts +4 -0
  8. package/dist/app-server-models.d.ts.map +1 -0
  9. package/dist/app-server-session.d.ts +3 -3
  10. package/dist/app-server-session.d.ts.map +1 -1
  11. package/dist/client-base.d.ts +5 -0
  12. package/dist/client-base.d.ts.map +1 -1
  13. package/dist/client-entry.js +3386 -639
  14. package/dist/client-entry.js.map +85 -13
  15. package/dist/cloud-client.d.ts +7 -0
  16. package/dist/cloud-client.d.ts.map +1 -0
  17. package/dist/cloud-management.d.ts +21 -18
  18. package/dist/cloud-management.d.ts.map +1 -1
  19. package/dist/cloud-session.d.ts +7 -5
  20. package/dist/cloud-session.d.ts.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3400 -653
  24. package/dist/index.js.map +86 -14
  25. package/dist/management-types.d.ts +85 -64
  26. package/dist/management-types.d.ts.map +1 -1
  27. package/dist/management.d.ts +11 -9
  28. package/dist/management.d.ts.map +1 -1
  29. package/dist/remote-session-protocol.d.ts +1 -1
  30. package/dist/remote-session-protocol.d.ts.map +1 -1
  31. package/dist/remote.d.ts +4 -5
  32. package/dist/remote.d.ts.map +1 -1
  33. package/dist/repositories.d.ts +3 -2
  34. package/dist/repositories.d.ts.map +1 -1
  35. package/dist/types.d.ts +22 -34
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +3 -2
  38. package/src/agent-repositories.ts +159 -0
  39. package/src/app-server-management.ts +56 -116
  40. package/src/app-server-models.ts +20 -0
  41. package/src/app-server-session.ts +71 -146
  42. package/src/client-base.ts +72 -20
  43. package/src/cloud-client.ts +46 -0
  44. package/src/cloud-management.ts +128 -275
  45. package/src/cloud-session.ts +168 -267
  46. package/src/index.ts +6 -0
  47. package/src/management-types.ts +107 -64
  48. package/src/management.ts +46 -39
  49. package/src/remote-session-protocol.ts +6 -2
  50. package/src/remote.ts +18 -63
  51. package/src/repositories.ts +26 -99
  52. package/src/types.ts +22 -35
@@ -1,3 +1,6 @@
1
+ import type Letta from "@letta-ai/letta-client";
2
+ import { APIError } from "@letta-ai/letta-client/core/error";
3
+ import type { AgentCreateParams } from "@letta-ai/letta-client/resources/agents/agents";
1
4
  import {
2
5
  createAppServerClient,
3
6
  type AppServerClient,
@@ -11,6 +14,12 @@ import {
11
14
  registerAppServerControlRequestHandler,
12
15
  } from "./app-server-session.js";
13
16
  import { createCloudStatusTransportConstructor } from "./cloud-status-transport.js";
17
+ import { CloudManagementTransport } from "./cloud-management.js";
18
+ import {
19
+ createCloudClient,
20
+ getCloudApiKey,
21
+ normalizeCloudApiBaseUrl,
22
+ } from "./cloud-client.js";
14
23
  import {
15
24
  RemoteEnvironmentClient,
16
25
  type RemoteEnvironmentTarget,
@@ -38,7 +47,6 @@ import {
38
47
  validateCloudSandboxOptions,
39
48
  } from "./cloud-sandbox.js";
40
49
 
41
- const DEFAULT_CLOUD_API_BASE_URL = "https://api.letta.com";
42
50
  const DEFAULT_TURN_TIMEOUT_MS = 120_000;
43
51
  const DEFAULT_PING_INTERVAL_MS = 30_000;
44
52
  const DEFAULT_SANDBOX_TTL_MINUTES = 5;
@@ -48,8 +56,6 @@ const DEFAULT_REPOSITORY_ATTACH_TIMEOUT_MS = 10_000;
48
56
  const DEFAULT_REPOSITORY_ATTACH_POLL_INTERVAL_MS = 250;
49
57
  const SDK_AGENT_ORIGIN = "@letta-ai/letta-agent-sdk";
50
58
 
51
- type FetchLike = typeof fetch;
52
-
53
59
  type CloudRuntimeStartResponse = ProtocolMessage & {
54
60
  type: "runtime_start_response";
55
61
  success: boolean;
@@ -126,37 +132,6 @@ export class CloudManagedSandboxExpiredError extends Error {
126
132
 
127
133
  type CloudSessionMode = Extract<RuntimeSessionMode, { kind: "session" }>;
128
134
 
129
- type CloudAgentRepository = {
130
- id: string;
131
- name?: string;
132
- is_primary?: boolean;
133
- };
134
-
135
- function getDefaultApiKey(): string | undefined {
136
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })
137
- .process?.env;
138
- return env?.LETTA_API_KEY ?? env?.LETTA_CLOUD_API_KEY;
139
- }
140
-
141
- function bearerTokenFromHeaders(headers: Record<string, string> | undefined): string | undefined {
142
- const authorization = headers?.Authorization ?? headers?.authorization;
143
- if (!authorization) return undefined;
144
- const match = /^Bearer\s+(.+)$/i.exec(authorization);
145
- return match?.[1];
146
- }
147
-
148
- function getCloudApiKey(options: LettaCodeCloudClientOptions): string | undefined {
149
- return options.apiKey ?? bearerTokenFromHeaders(options.headers) ?? getDefaultApiKey();
150
- }
151
-
152
- function getFetch(fetchOverride?: FetchLike): FetchLike {
153
- const resolved = fetchOverride ?? globalThis.fetch;
154
- if (!resolved) {
155
- throw new Error("No fetch implementation available for cloud backend.");
156
- }
157
- return resolved.bind(globalThis) as FetchLike;
158
- }
159
-
160
135
  function getWebSocketConstructor(
161
136
  websocketOverride?: LettaCodeSocketConstructor,
162
137
  ): LettaCodeSocketConstructor {
@@ -169,26 +144,6 @@ function getWebSocketConstructor(
169
144
  return resolved;
170
145
  }
171
146
 
172
- function normalizeCloudApiBaseUrl(url: string | undefined): string {
173
- const parsed = new URL(url ?? DEFAULT_CLOUD_API_BASE_URL);
174
- parsed.pathname = parsed.pathname.replace(/\/+$/, "");
175
- parsed.search = "";
176
- parsed.hash = "";
177
- return parsed.toString().replace(/\/$/, "");
178
- }
179
-
180
- function cloudHeaders(options: LettaCodeCloudClientOptions): Record<string, string> {
181
- const headers: Record<string, string> = {
182
- "Content-Type": "application/json",
183
- ...(options.headers ?? {}),
184
- };
185
- const apiKey = getCloudApiKey(options);
186
- if (apiKey && !headers.Authorization && !headers.authorization) {
187
- headers.Authorization = `Bearer ${apiKey}`;
188
- }
189
- return headers;
190
- }
191
-
192
147
  function cloudWebSocketHeaders(options: LettaCodeCloudClientOptions): Record<string, string> | undefined {
193
148
  const headers = { ...(options.headers ?? {}) };
194
149
  delete headers.authorization;
@@ -198,45 +153,8 @@ function cloudWebSocketHeaders(options: LettaCodeCloudClientOptions): Record<str
198
153
  return Object.keys(headers).length > 0 ? headers : undefined;
199
154
  }
200
155
 
201
- async function parseJsonResponse(response: Response): Promise<unknown> {
202
- const text = await response.text();
203
- if (!text) return null;
204
- try {
205
- return JSON.parse(text) as unknown;
206
- } catch {
207
- return text;
208
- }
209
- }
210
-
211
- function responseErrorMessage(body: unknown, fallback: string): string {
212
- if (body && typeof body === "object") {
213
- const record = body as Record<string, unknown>;
214
- const message = record.message ?? record.error ?? record.detail;
215
- const reasonText = record.reason_text;
216
- const pieces = [message, reasonText]
217
- .filter((value): value is string => typeof value === "string" && value.length > 0);
218
- if (pieces.length > 0) return pieces.join(": ");
219
- }
220
- return fallback;
221
- }
222
-
223
- function assertOkResponse(response: Response, body: unknown, action: string, url?: string): void {
224
- if (!response.ok) {
225
- const detail = responseErrorMessage(body, `HTTP ${response.status}`);
226
- const parts = [`${action} failed`, detail];
227
- if (url) parts.push(`URL: ${url}`);
228
- if (response.status === 401 || response.status === 403) {
229
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })
230
- .process?.env;
231
- const hasKey = !!(env?.LETTA_API_KEY ?? env?.LETTA_CLOUD_API_KEY);
232
- parts.push(
233
- hasKey
234
- ? "Authentication failed — the API key may be invalid or lack permissions for this resource."
235
- : "No API key found. Set LETTA_API_KEY (or pass apiKey in client options).",
236
- );
237
- }
238
- throw new Error(parts.join(" — "));
239
- }
156
+ function isNotFound(error: unknown): boolean {
157
+ return error instanceof APIError && error.status === 404;
240
158
  }
241
159
 
242
160
  function validatePositiveInteger(value: number | undefined, name: string): void {
@@ -357,31 +275,15 @@ function externalToolsByName(tools: AnyAgentTool[] | undefined): Map<string, Any
357
275
  }
358
276
 
359
277
  export async function createCloudAgent(
360
- clientOptions: LettaCodeCloudClientOptions,
278
+ client: Letta,
361
279
  agentOptions: CreateAgentOptions,
362
280
  ): Promise<string> {
363
281
  const body = await createAgentBody(agentOptions);
364
-
365
- // No trailing slash: production api.letta.com 404s `POST /v1/agents/` at
366
- // the router (GET tolerates the slash; POST does not).
367
- const response = await getFetch(clientOptions.fetch)(
368
- `${normalizeCloudApiBaseUrl(clientOptions.apiBaseUrl)}/v1/agents`,
369
- {
370
- method: "POST",
371
- headers: cloudHeaders(clientOptions),
372
- body: JSON.stringify(body),
373
- },
374
- );
375
- const responseBody = await parseJsonResponse(response);
376
- assertOkResponse(response, responseBody, "Cloud create agent");
377
- const agentId =
378
- responseBody && typeof responseBody === "object"
379
- ? (responseBody as { id?: unknown }).id
380
- : undefined;
381
- if (typeof agentId !== "string" || agentId.length === 0) {
282
+ const agent = await client.agents.create(body as AgentCreateParams);
283
+ if (typeof agent.id !== "string" || agent.id.length === 0) {
382
284
  throw new Error("Cloud create agent response did not include an agent id.");
383
285
  }
384
- return agentId;
286
+ return agent.id;
385
287
  }
386
288
 
387
289
  export function assertCloudSessionOptionsSupported(
@@ -404,24 +306,36 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
404
306
  private sandboxRefreshInFlight: Promise<void> | null = null;
405
307
  private sandboxLifecycleClosing = false;
406
308
  private attachedRepositoryIds = new Set<string>();
309
+ private repositoryIdsRequiringCleanupRecompile = new Set<string>();
310
+ private readonly repositoryManagement: CloudManagementTransport;
407
311
  private readonly cloudMode: CloudSessionMode;
408
312
 
409
313
  constructor(
410
314
  private readonly cloudOptions: LettaCodeCloudClientOptions,
411
315
  mode: CloudSessionMode,
316
+ private readonly apiClient: Letta = createCloudClient(cloudOptions),
412
317
  ) {
413
318
  super(mode, {
414
319
  label: "cloud",
415
320
  requestTimeoutMs: cloudOptions.requestTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS,
416
321
  });
417
322
  this.cloudMode = mode;
323
+ this.repositoryManagement = new CloudManagementTransport(apiClient);
418
324
  const tools = mode.options.tools;
419
325
  this.externalTools = externalToolsByName(tools);
420
326
  }
421
327
 
422
328
  protected override async initializeRuntimeController(): Promise<RuntimeSessionInit> {
423
329
  const resolved = await this.resolveRuntime();
424
- const connection = await this.resolveConnectionForRuntime(resolved.runtime);
330
+ const connection = await this.resolveConnectionForRuntime(resolved.runtime).catch(
331
+ async (error: unknown) => {
332
+ await this.cleanupSessionRepositories(
333
+ resolved.runtime.agent_id,
334
+ resolved.runtime.conversation_id,
335
+ );
336
+ throw error;
337
+ },
338
+ );
425
339
  this.connectionId = connection.connectionId;
426
340
 
427
341
  const apiKey = getCloudApiKey(this.cloudOptions);
@@ -488,7 +402,10 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
488
402
  this.removeControlRequestHandler = null;
489
403
  client.close();
490
404
  await this.cleanupManagedSandbox();
491
- await this.cleanupSessionRepositories(resolved.runtime.agent_id);
405
+ await this.cleanupSessionRepositories(
406
+ resolved.runtime.agent_id,
407
+ resolved.runtime.conversation_id,
408
+ );
492
409
  throw error;
493
410
  }
494
411
  }
@@ -545,7 +462,12 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
545
462
  this.removeControlRequestHandler?.();
546
463
  this.removeControlRequestHandler = null;
547
464
  void this.cleanupManagedSandbox();
548
- if (this.runtime?.agent_id) void this.cleanupSessionRepositories(this.runtime.agent_id);
465
+ if (this.runtime?.agent_id) {
466
+ void this.cleanupSessionRepositories(
467
+ this.runtime.agent_id,
468
+ this.runtime.conversation_id,
469
+ );
470
+ }
549
471
  }
550
472
 
551
473
  private async resolveRuntime(): Promise<{ runtime: RuntimeScope }> {
@@ -566,29 +488,41 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
566
488
  );
567
489
  }
568
490
 
569
- await this.attachSessionRepositories(agentId);
491
+ const shouldRecompileRepositories =
492
+ await this.attachSessionRepositories(agentId);
570
493
 
571
- if (this.cloudMode.newConversation) {
572
- const conversation = await this.createConversation(agentId);
573
- conversationId = conversation.id;
574
- } else if (this.cloudMode.defaultConversation) {
575
- conversationId = "default";
576
- }
494
+ try {
495
+ if (this.cloudMode.newConversation) {
496
+ const conversation = await this.createConversation(agentId);
497
+ conversationId = conversation.id;
498
+ } else if (this.cloudMode.defaultConversation) {
499
+ conversationId = "default";
500
+ }
577
501
 
578
- if (!conversationId) {
579
- throw new Error(
580
- "Letta Cloud createSession()/resumeSession() requires an agent id or conversation id.",
581
- );
582
- }
502
+ if (!conversationId) {
503
+ throw new Error(
504
+ "Letta Cloud createSession()/resumeSession() requires an agent id or conversation id.",
505
+ );
506
+ }
507
+
508
+ if (shouldRecompileRepositories) {
509
+ await this.recompileSystemPrompt(agentId, conversationId);
510
+ }
583
511
 
584
- return { runtime: { agent_id: agentId, conversation_id: conversationId } };
512
+ return { runtime: { agent_id: agentId, conversation_id: conversationId } };
513
+ } catch (error) {
514
+ await this.cleanupSessionRepositories(agentId, conversationId);
515
+ throw error;
516
+ }
585
517
  }
586
518
 
587
- private async attachSessionRepositories(agentId: string): Promise<void> {
519
+ private async attachSessionRepositories(agentId: string): Promise<boolean> {
588
520
  const resources = this.repositoryResources();
589
- if (resources.length === 0) return;
521
+ if (resources.length === 0) return false;
590
522
 
591
- const existing = await this.listAgentRepositories(agentId);
523
+ const existing = await this.repositoryManagement.listAgentRepositories(
524
+ agentId,
525
+ );
592
526
  const existingIds = new Set(existing.map((repository) => repository.id));
593
527
 
594
528
  try {
@@ -596,7 +530,11 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
596
530
  if (existingIds.has(resource.repositoryId) || this.attachedRepositoryIds.has(resource.repositoryId)) {
597
531
  continue;
598
532
  }
599
- await this.linkAgentRepository(agentId, resource.repositoryId);
533
+ await this.repositoryManagement.attachAgentRepository(
534
+ agentId,
535
+ resource.repositoryId,
536
+ undefined,
537
+ );
600
538
  await this.waitForAgentRepository(agentId, resource.repositoryId);
601
539
  this.attachedRepositoryIds.add(resource.repositoryId);
602
540
  }
@@ -604,18 +542,70 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
604
542
  await this.cleanupSessionRepositories(agentId);
605
543
  throw error;
606
544
  }
545
+
546
+ // Recompile even when every requested repository was already linked: the
547
+ // target conversation may still have been compiled before those links
548
+ // existed. One recompile after the full batch produces the desired state.
549
+ const shouldRecompile = resources.some(
550
+ (resource) => resource.recompile !== false,
551
+ );
552
+ this.repositoryIdsRequiringCleanupRecompile = new Set(
553
+ resources
554
+ .filter(
555
+ (resource) =>
556
+ resource.recompile !== false &&
557
+ this.attachedRepositoryIds.has(resource.repositoryId),
558
+ )
559
+ .map((resource) => resource.repositoryId),
560
+ );
561
+ return shouldRecompile;
607
562
  }
608
563
 
609
- private async cleanupSessionRepositories(agentId: string): Promise<void> {
564
+ private async cleanupSessionRepositories(
565
+ agentId: string,
566
+ conversationId?: string,
567
+ ): Promise<void> {
610
568
  const repositoryIds = [...this.attachedRepositoryIds];
569
+ const repositoryIdsRequiringRecompile =
570
+ this.repositoryIdsRequiringCleanupRecompile;
611
571
  this.attachedRepositoryIds.clear();
612
- await Promise.all(repositoryIds.map(async (repositoryId) => {
572
+ this.repositoryIdsRequiringCleanupRecompile = new Set<string>();
573
+ const recompileAfterDetach = await Promise.all(repositoryIds.map(async (repositoryId) => {
613
574
  try {
614
- await this.unlinkAgentRepository(agentId, repositoryId);
575
+ await this.repositoryManagement.detachAgentRepository(
576
+ agentId,
577
+ repositoryId,
578
+ );
579
+ return repositoryIdsRequiringRecompile.has(repositoryId);
615
580
  } catch {
616
581
  // Best-effort cleanup: only repositories this SDK session attached are removed.
582
+ return false;
617
583
  }
618
584
  }));
585
+ // Best-effort recompile after detach so remaining conversations drop stale
586
+ // repository projections. Errors are swallowed because this runs on close.
587
+ if (recompileAfterDetach.some(Boolean)) {
588
+ try {
589
+ await this.recompileSystemPrompt(agentId, conversationId);
590
+ } catch {
591
+ // Best-effort: cleanup must not throw.
592
+ }
593
+ }
594
+ }
595
+
596
+ private async recompileSystemPrompt(
597
+ agentId: string,
598
+ conversationId?: string,
599
+ ): Promise<void> {
600
+ const usesAgentPrompt = !conversationId || conversationId === "default";
601
+ if (usesAgentPrompt) {
602
+ await this.repositoryManagement.recompileAgentSystemPrompt(agentId);
603
+ return;
604
+ }
605
+ await this.repositoryManagement.recompileConversationSystemPrompt(
606
+ agentId,
607
+ conversationId,
608
+ );
619
609
  }
620
610
 
621
611
  private repositoryResources(): RepositoryResource[] {
@@ -636,30 +626,12 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
636
626
  return result;
637
627
  }
638
628
 
639
- private async listAgentRepositories(agentId: string): Promise<CloudAgentRepository[]> {
640
- const fetchImpl = getFetch(this.cloudOptions.fetch);
641
- const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
642
- const response = await fetchImpl(
643
- `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/repositories`,
644
- { headers: cloudHeaders(this.cloudOptions) },
645
- );
646
- const body = await parseJsonResponse(response);
647
- assertOkResponse(response, body, "Cloud list agent repositories");
648
- if (!body || typeof body !== "object") return [];
649
- const repositories = (body as Record<string, unknown>).repositories;
650
- return Array.isArray(repositories)
651
- ? repositories.filter((repository): repository is CloudAgentRepository => (
652
- repository !== null &&
653
- typeof repository === "object" &&
654
- typeof (repository as Record<string, unknown>).id === "string"
655
- ))
656
- : [];
657
- }
658
-
659
629
  private async waitForAgentRepository(agentId: string, repositoryId: string): Promise<void> {
660
630
  const deadline = Date.now() + DEFAULT_REPOSITORY_ATTACH_TIMEOUT_MS;
661
631
  while (true) {
662
- const repositories = await this.listAgentRepositories(agentId);
632
+ const repositories = await this.repositoryManagement.listAgentRepositories(
633
+ agentId,
634
+ );
663
635
  if (repositories.some((repository) => repository.id === repositoryId)) return;
664
636
  if (Date.now() >= deadline) {
665
637
  throw new Error(`Cloud attach agent repository did not become visible for ${agentId}: ${repositoryId}`);
@@ -668,64 +640,24 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
668
640
  }
669
641
  }
670
642
 
671
- private async linkAgentRepository(agentId: string, repositoryId: string): Promise<void> {
672
- const fetchImpl = getFetch(this.cloudOptions.fetch);
673
- const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
674
- const response = await fetchImpl(
675
- `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/repositories`,
676
- {
677
- method: "POST",
678
- headers: cloudHeaders(this.cloudOptions),
679
- body: JSON.stringify({ repository_id: repositoryId }),
680
- },
681
- );
682
- const body = await parseJsonResponse(response);
683
- assertOkResponse(response, body, "Cloud attach agent repository");
684
- }
685
-
686
- private async unlinkAgentRepository(agentId: string, repositoryId: string): Promise<void> {
687
- const fetchImpl = getFetch(this.cloudOptions.fetch);
688
- const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
689
- const response = await fetchImpl(
690
- `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/repositories/${encodeURIComponent(repositoryId)}`,
691
- { method: "DELETE", headers: cloudHeaders(this.cloudOptions) },
692
- );
693
- const body = await parseJsonResponse(response);
694
- if (response.status === 404) return;
695
- assertOkResponse(response, body, "Cloud detach agent repository");
696
- }
697
-
698
643
  private async createConversation(agentId: string): Promise<{ id: string; agent_id?: string }> {
699
- const fetchImpl = getFetch(this.cloudOptions.fetch);
700
- const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
701
- const url = new URL(`${baseUrl}/v1/conversations/`);
702
- url.searchParams.set("agent_id", agentId);
703
- const response = await fetchImpl(url, {
704
- method: "POST",
705
- headers: cloudHeaders(this.cloudOptions),
706
- body: JSON.stringify({}),
644
+ const conversation = await this.apiClient.conversations.create({
645
+ agent_id: agentId,
707
646
  });
708
- const body = await parseJsonResponse(response);
709
- assertOkResponse(response, body, "Cloud createSession()", url.toString());
710
- if (!isCloudConversation(body)) {
647
+ if (!isCloudConversation(conversation)) {
711
648
  throw new Error("Cloud createSession() response did not include a conversation id.");
712
649
  }
713
- return { id: body.id, agent_id: body.agent_id };
650
+ return { id: conversation.id, agent_id: conversation.agent_id };
714
651
  }
715
652
 
716
653
  private async retrieveConversation(conversationId: string): Promise<{ id: string; agent_id?: string }> {
717
- const fetchImpl = getFetch(this.cloudOptions.fetch);
718
- const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
719
- const response = await fetchImpl(
720
- `${baseUrl}/v1/conversations/${encodeURIComponent(conversationId)}`,
721
- { headers: cloudHeaders(this.cloudOptions) },
654
+ const conversation = await this.apiClient.conversations.retrieve(
655
+ conversationId,
722
656
  );
723
- const body = await parseJsonResponse(response);
724
- assertOkResponse(response, body, "Cloud resumeSession()", `${baseUrl}/v1/conversations/${encodeURIComponent(conversationId)}`);
725
- if (!isCloudConversation(body)) {
657
+ if (!isCloudConversation(conversation)) {
726
658
  throw new Error(`Cloud resumeSession() could not retrieve conversation ${conversationId}.`);
727
659
  }
728
- return { id: body.id, agent_id: body.agent_id };
660
+ return { id: conversation.id, agent_id: conversation.agent_id };
729
661
  }
730
662
 
731
663
  private async resolveConnectionForRuntime(
@@ -787,25 +719,19 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
787
719
  agentId: string,
788
720
  conversationId?: string,
789
721
  ): Promise<ManagedCloudSandbox> {
790
- const fetchImpl = getFetch(this.cloudOptions.fetch);
791
- const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
792
722
  const sandboxOptions = this.resolvedSandboxOptions();
793
723
  const githubRepositories = sandboxOptions.githubRepositories;
794
- const response = await fetchImpl(
795
- `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sandboxes`,
724
+ const body = await this.apiClient.post<unknown>(
725
+ `/v1/agents/${encodeURIComponent(agentId)}/sandboxes`,
796
726
  {
797
- method: "POST",
798
- headers: cloudHeaders(this.cloudOptions),
799
- body: JSON.stringify({
727
+ body: {
800
728
  ...(conversationId ? { conversationId } : {}),
801
729
  ...(githubRepositories && githubRepositories.length > 0
802
730
  ? { githubRepositories }
803
731
  : {}),
804
- }),
732
+ },
805
733
  },
806
734
  );
807
- const body = await parseJsonResponse(response);
808
- assertOkResponse(response, body, "Cloud create managed sandbox", `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sandboxes`);
809
735
  if (!isCloudAgentSandbox(body)) {
810
736
  throw new Error("Cloud create managed sandbox response did not include sandbox connection details.");
811
737
  }
@@ -863,22 +789,17 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
863
789
  }
864
790
 
865
791
  private async refreshManagedSandboxOnce(sandbox: ManagedCloudSandbox): Promise<void> {
866
- const fetchImpl = getFetch(this.cloudOptions.fetch);
867
- const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
868
-
869
792
  if (sandbox.conversationId) {
870
793
  // Conversation-scoped sandboxes refresh by id — no "latest active"
871
794
  // indirection, so ownership changes are structurally impossible.
872
- const response = await fetchImpl(
873
- `${baseUrl}/v1/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/refresh`,
874
- {
875
- method: "POST",
876
- headers: cloudHeaders(this.cloudOptions),
877
- body: JSON.stringify({ ttlMinutes: sandbox.ttlMinutes }),
878
- },
879
- );
880
- if (response.status === 404) {
881
- await parseJsonResponse(response);
795
+ let body: unknown;
796
+ try {
797
+ body = await this.apiClient.post(
798
+ `/v1/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/refresh`,
799
+ { body: { ttlMinutes: sandbox.ttlMinutes } },
800
+ );
801
+ } catch (error) {
802
+ if (!isNotFound(error)) throw error;
882
803
  // The current control and stream WebSockets are bound to this
883
804
  // sandbox's ephemeral connection id. Recreating only the sandbox would
884
805
  // leave both sockets targeting the dead connection, so surface a
@@ -889,24 +810,18 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
889
810
  sandbox.conversationId,
890
811
  );
891
812
  }
892
- const body = await parseJsonResponse(response);
893
- assertOkResponse(response, body, "Cloud refresh managed sandbox");
894
813
  if (!isCloudAgentSandboxRefresh(body) || !body.success) {
895
814
  throw new Error("Cloud refresh managed sandbox response did not confirm refresh.");
896
815
  }
897
816
  return;
898
817
  }
899
818
 
900
- const response = await fetchImpl(
901
- `${baseUrl}/v1/agents/${encodeURIComponent(sandbox.agentId)}/sandboxes/refresh`,
819
+ const body = await this.apiClient.post<unknown>(
820
+ `/v1/agents/${encodeURIComponent(sandbox.agentId)}/sandboxes/refresh`,
902
821
  {
903
- method: "POST",
904
- headers: cloudHeaders(this.cloudOptions),
905
- body: JSON.stringify({ ttlMinutes: sandbox.ttlMinutes }),
822
+ body: { ttlMinutes: sandbox.ttlMinutes },
906
823
  },
907
824
  );
908
- const body = await parseJsonResponse(response);
909
- assertOkResponse(response, body, "Cloud refresh managed sandbox", `${baseUrl}/v1/agents/${encodeURIComponent(sandbox.agentId)}/sandboxes/refresh`);
910
825
  if (!isCloudAgentSandboxRefresh(body) || !body.success) {
911
826
  throw new Error("Cloud refresh managed sandbox response did not confirm refresh.");
912
827
  }
@@ -918,23 +833,17 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
918
833
  }
919
834
 
920
835
  private async terminateManagedSandbox(sandbox: ManagedCloudSandbox): Promise<void> {
921
- const fetchImpl = getFetch(this.cloudOptions.fetch);
922
- const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
923
-
924
836
  if (sandbox.conversationId) {
925
837
  // Terminate exactly this sandbox — the agent-scoped DELETE targets the
926
838
  // agent's "latest active" sandbox, which may not be ours.
927
- const response = await fetchImpl(
928
- `${baseUrl}/v1/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/terminate`,
929
- {
930
- method: "POST",
931
- headers: cloudHeaders(this.cloudOptions),
932
- body: JSON.stringify({}),
933
- },
934
- );
935
- const body = await parseJsonResponse(response);
936
- if (response.status === 404) return;
937
- assertOkResponse(response, body, "Cloud terminate managed sandbox");
839
+ try {
840
+ await this.apiClient.post(
841
+ `/v1/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/terminate`,
842
+ { body: {} },
843
+ );
844
+ } catch (error) {
845
+ if (!isNotFound(error)) throw error;
846
+ }
938
847
  return;
939
848
  }
940
849
 
@@ -942,16 +851,13 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
942
851
  // "latest active" sandbox the DELETE will target is still ours.
943
852
  await this.refreshManagedSandbox(sandbox);
944
853
 
945
- const response = await fetchImpl(
946
- `${baseUrl}/v1/agents/${encodeURIComponent(sandbox.agentId)}/sandboxes`,
947
- {
948
- method: "DELETE",
949
- headers: cloudHeaders(this.cloudOptions),
950
- },
951
- );
952
- const body = await parseJsonResponse(response);
953
- if (response.status === 404) return;
954
- assertOkResponse(response, body, "Cloud terminate managed sandbox", `${baseUrl}/v1/agents/${encodeURIComponent(sandbox.agentId)}/sandboxes`);
854
+ try {
855
+ await this.apiClient.delete(
856
+ `/v1/agents/${encodeURIComponent(sandbox.agentId)}/sandboxes`,
857
+ );
858
+ } catch (error) {
859
+ if (!isNotFound(error)) throw error;
860
+ }
955
861
  }
956
862
 
957
863
  private async waitForManagedSandboxConnection(
@@ -1027,12 +933,7 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
1027
933
  }
1028
934
 
1029
935
  private remoteEnvironmentClient(): RemoteEnvironmentClient {
1030
- return new RemoteEnvironmentClient({
1031
- baseUrl: this.cloudOptions.apiBaseUrl,
1032
- apiKey: getCloudApiKey(this.cloudOptions),
1033
- headers: this.cloudOptions.headers,
1034
- fetch: this.cloudOptions.fetch,
1035
- });
936
+ return new RemoteEnvironmentClient({}, this.apiClient);
1036
937
  }
1037
938
 
1038
939
  private effectiveEnvironment(): LettaCodeEnvironment | undefined {
package/src/index.ts CHANGED
@@ -145,6 +145,10 @@ export type {
145
145
  AnyAgentTool,
146
146
  } from "./types.js";
147
147
  export type {
148
+ AgentRepositoriesClient,
149
+ AgentRepository,
150
+ AgentRepositoryRecompileTarget,
151
+ AttachAgentRepositoryOptions,
148
152
  AgentsClient,
149
153
  ConversationsClient,
150
154
  LettaAgent,
@@ -158,6 +162,8 @@ export type {
158
162
  UpdateConversationOptions,
159
163
  ConversationMessagesOptions,
160
164
  ConversationMessagesResult,
165
+ DetachAgentRepositoryOptions,
166
+ AgentRepositoryPermissions,
161
167
  } from "./management-types.js";
162
168
 
163
169
  export { RepositoriesClient } from "./repositories.js";