@letta-ai/letta-agent-sdk 0.6.2 → 0.6.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.
@@ -171,8 +171,19 @@ function validatePositiveInteger(value: number | undefined, name: string): void
171
171
  export function validateCloudClientOptions(options: LettaCodeCloudClientOptions): void {
172
172
  validatePositiveInteger(options.requestTimeoutMs, "requestTimeoutMs");
173
173
  validateCloudSandboxOptions(options.sandbox, "sandbox");
174
- if (options.environment !== undefined && options.sandbox !== undefined) {
175
- throw new Error("Letta Cloud sessions cannot specify both environment and sandbox options.");
174
+ if (options.computer !== undefined && options.environment !== undefined) {
175
+ throw new Error(
176
+ "Letta Cloud clients cannot specify both computer and deprecated environment.",
177
+ );
178
+ }
179
+ if (
180
+ (options.computer !== undefined || options.environment !== undefined) &&
181
+ options.sandbox !== undefined
182
+ ) {
183
+ const field = options.computer !== undefined ? "computer" : "environment";
184
+ throw new Error(
185
+ `Letta Cloud sessions cannot specify both ${field} and sandbox options.`,
186
+ );
176
187
  }
177
188
  if (
178
189
  options.webSocketAuth !== undefined &&
@@ -346,12 +357,44 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
346
357
  );
347
358
  this.connectionId = connection.connectionId;
348
359
 
360
+ const options = this.currentOptions();
361
+ this.mcpBridge = await connectMcpServers(
362
+ "mcpServers" in options ? options.mcpServers : undefined,
363
+ {
364
+ cwd: options.cwd,
365
+ reservedToolNames: this.externalTools.keys(),
366
+ },
367
+ );
368
+ for (const tool of this.mcpBridge.tools) {
369
+ this.externalTools.set(tool.name, tool);
370
+ }
371
+
372
+ try {
373
+ return await this.openCloudTransport(
374
+ resolved.runtime,
375
+ connection.connectionId,
376
+ );
377
+ } catch (error) {
378
+ await this.closeMcpBridge();
379
+ await this.cleanupManagedSandbox();
380
+ await this.cleanupSessionRepositories(
381
+ resolved.runtime.agent_id,
382
+ resolved.runtime.conversation_id,
383
+ );
384
+ throw error;
385
+ }
386
+ }
387
+
388
+ private async openCloudTransport(
389
+ runtime: RuntimeScope,
390
+ connectionId: string,
391
+ ): Promise<RuntimeSessionInit> {
349
392
  const apiKey = getCloudApiKey(this.cloudOptions);
350
393
  const url = buildCloudStatusWebSocketUrl({
351
394
  apiBaseUrl: this.cloudOptions.apiBaseUrl,
352
- connectionId: connection.connectionId,
353
- agentId: resolved.runtime.agent_id,
354
- conversationId: resolved.runtime.conversation_id,
395
+ connectionId,
396
+ agentId: runtime.agent_id,
397
+ conversationId: runtime.conversation_id,
355
398
  apiKey,
356
399
  authMode: this.cloudOptions.webSocketAuth ?? "header",
357
400
  });
@@ -365,40 +408,21 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
365
408
  : {}),
366
409
  pingIntervalMs:
367
410
  this.cloudOptions.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS,
368
- runtime: resolved.runtime,
411
+ runtime,
369
412
  }),
370
413
  requestTimeoutMs: this.cloudOptions.requestTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS,
371
414
  }));
372
415
  const options = this.currentOptions();
373
- this.mcpBridge = await connectMcpServers(
374
- "mcpServers" in options ? options.mcpServers : undefined,
375
- {
376
- cwd: options.cwd,
377
- reservedToolNames: this.externalTools.keys(),
378
- },
379
- );
380
- for (const tool of this.mcpBridge.tools) {
381
- this.externalTools.set(tool.name, tool);
382
- }
383
- this.removeControlRequestHandler = registerAppServerControlRequestHandler({
384
- client,
385
- getRuntime: () => this.runtime,
386
- getOptions: () => this.currentOptions(),
387
- });
388
- if (this.externalTools.size > 0) {
389
- this.removeExternalToolHandler = client.onExternalToolCall(
390
- createExternalToolCallHandler(this.externalTools),
391
- );
392
- }
416
+ this.installTransportHandlers(client);
393
417
 
394
418
  try {
395
419
  await client.connect();
396
- const response = await this.startCloudRuntime(client, resolved.runtime);
420
+ const response = await this.startCloudRuntime(client, runtime);
397
421
  if (!response.success || !response.runtime) {
398
422
  throw new Error(response.error ?? "Failed to start Cloud status runtime");
399
423
  }
400
424
 
401
- this.watchTransportDisconnect(client);
425
+ this.watchTransportDisconnect(client, { recoverWhenIdle: true });
402
426
 
403
427
  const tools = agentToolNames(response.agent);
404
428
  const skillSources = options.skillSources;
@@ -428,21 +452,64 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
428
452
  ...(skillSources !== undefined ? { skillSources: [...skillSources] } : {}),
429
453
  };
430
454
  } catch (error) {
431
- this.removeExternalToolHandler?.();
432
- this.removeExternalToolHandler = null;
433
- this.removeControlRequestHandler?.();
434
- this.removeControlRequestHandler = null;
435
- await this.closeMcpBridge();
455
+ this.cleanupTransportHandlers();
436
456
  client.close();
437
- await this.cleanupManagedSandbox();
438
- await this.cleanupSessionRepositories(
439
- resolved.runtime.agent_id,
440
- resolved.runtime.conversation_id,
441
- );
442
457
  throw error;
443
458
  }
444
459
  }
445
460
 
461
+ private installTransportHandlers(client: AppServerClient): void {
462
+ this.cleanupTransportHandlers();
463
+ this.removeControlRequestHandler = registerAppServerControlRequestHandler({
464
+ client,
465
+ getRuntime: () => this.runtime,
466
+ getOptions: () => this.currentOptions(),
467
+ });
468
+ if (this.externalTools.size > 0) {
469
+ this.removeExternalToolHandler = client.onExternalToolCall(
470
+ createExternalToolCallHandler(this.externalTools),
471
+ );
472
+ }
473
+ }
474
+
475
+ private cleanupTransportHandlers(): void {
476
+ this.removeExternalToolHandler?.();
477
+ this.removeExternalToolHandler = null;
478
+ this.removeControlRequestHandler?.();
479
+ this.removeControlRequestHandler = null;
480
+ }
481
+
482
+ protected override async recoverIdleTransport(
483
+ runtime: RuntimeScope,
484
+ ): Promise<RuntimeSessionInit> {
485
+ const connection = await this.resolveRecoveryConnection(runtime);
486
+ this.connectionId = connection.connectionId;
487
+ return this.openCloudTransport(runtime, connection.connectionId);
488
+ }
489
+
490
+ private async resolveRecoveryConnection(
491
+ runtime: RuntimeScope,
492
+ ): Promise<ResolvedCloudConnection> {
493
+ const sandbox = this.managedSandbox;
494
+ if (sandbox) {
495
+ await this.refreshManagedSandbox(sandbox);
496
+ return this.waitForManagedSandboxConnection(sandbox);
497
+ }
498
+ const environment = this.effectiveEnvironment();
499
+ if (!environment) {
500
+ throw new Error("Cloud idle transport recovery lost its execution target");
501
+ }
502
+ return this.resolveExplicitConnection(environment);
503
+ }
504
+
505
+ protected override onIdleTransportDisconnect(): void {
506
+ this.cleanupTransportHandlers();
507
+ }
508
+
509
+ protected override onRecoveredTransportDiscarded(): void {
510
+ this.cleanupTransportHandlers();
511
+ }
512
+
446
513
  private async startCloudRuntime(
447
514
  client: AppServerClient,
448
515
  runtime: RuntimeScope,
@@ -490,10 +557,7 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
490
557
  }
491
558
 
492
559
  protected override onCoreClose(): void {
493
- this.removeExternalToolHandler?.();
494
- this.removeExternalToolHandler = null;
495
- this.removeControlRequestHandler?.();
496
- this.removeControlRequestHandler = null;
560
+ this.cleanupTransportHandlers();
497
561
  this.mcpCleanup = this.closeMcpBridge();
498
562
  void this.cleanupManagedSandbox();
499
563
  if (this.runtime?.agent_id) {
@@ -982,10 +1046,12 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
982
1046
  }
983
1047
 
984
1048
  private effectiveEnvironment(): LettaCodeEnvironment | undefined {
985
- const modeEnvironment = this.mode.kind === "session"
986
- ? this.mode.options.environment
1049
+ const modeComputer = this.mode.kind === "session"
1050
+ ? this.mode.options.computer ?? this.mode.options.environment
987
1051
  : undefined;
988
- return modeEnvironment ?? this.cloudOptions.environment;
1052
+ return modeComputer ??
1053
+ this.cloudOptions.computer ??
1054
+ this.cloudOptions.environment;
989
1055
  }
990
1056
 
991
1057
  private effectiveSandboxOptions(): LettaCodeCloudSandboxOptions | undefined {
@@ -0,0 +1,132 @@
1
+ import type { Letta } from "@letta-ai/letta-client";
2
+ import {
3
+ RemoteEnvironmentClient,
4
+ type RemoteEnvironmentConnection,
5
+ type RemoteEnvironmentTarget,
6
+ } from "./remote.js";
7
+
8
+ /**
9
+ * Selects the computer where a Cloud session runs tools and accesses files.
10
+ *
11
+ * Strings are treated as human-readable computer names. Prefer `deviceId` when
12
+ * persisting a selection because connection IDs can rotate after reconnects.
13
+ */
14
+ export type ComputerSelector =
15
+ | string
16
+ | { name: string }
17
+ | { id: string }
18
+ | { connectionId: string }
19
+ | { deviceId: string };
20
+
21
+ export interface ComputerMetadata {
22
+ os?: string;
23
+ lettaCodeVersion?: string;
24
+ nodeVersion?: string;
25
+ workingDirectory?: string;
26
+ gitBranch?: string;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ /** A registered computer that can host Letta agent tool execution. */
31
+ export interface Computer {
32
+ /** Environment record ID. */
33
+ id: string;
34
+ /** Stable identifier for the physical device across reconnects. */
35
+ deviceId: string;
36
+ /** Current online connection lease. This may rotate after reconnects. */
37
+ connectionId: string | null;
38
+ /** Human-readable computer name. */
39
+ name: string;
40
+ status: "online" | "offline";
41
+ connectedAt: number | null;
42
+ lastSeenAt: number;
43
+ metadata?: ComputerMetadata;
44
+ }
45
+
46
+ export interface ListComputersOptions {
47
+ limit?: number;
48
+ after?: string;
49
+ onlineOnly?: boolean;
50
+ }
51
+
52
+ export interface ListComputersResult {
53
+ computers: Computer[];
54
+ hasNextPage: boolean;
55
+ }
56
+
57
+ export interface ResolvedComputer {
58
+ connectionId: string;
59
+ /** Omitted when resolving an explicit connection ID. */
60
+ computer?: Computer;
61
+ }
62
+
63
+ export interface ComputersClient {
64
+ list(options?: ListComputersOptions): Promise<ListComputersResult>;
65
+ get(deviceId: string): Promise<Computer>;
66
+ resolve(selector: ComputerSelector): Promise<ResolvedComputer>;
67
+ }
68
+
69
+ function selectorToRemoteTarget(
70
+ selector: ComputerSelector,
71
+ ): RemoteEnvironmentTarget {
72
+ if (typeof selector === "string") return { connectionName: selector };
73
+ if ("name" in selector) return { connectionName: selector.name };
74
+ if ("id" in selector) return { environmentId: selector.id };
75
+ if ("connectionId" in selector) {
76
+ return { connectionId: selector.connectionId };
77
+ }
78
+ return { deviceId: selector.deviceId };
79
+ }
80
+
81
+ function toComputer(environment: RemoteEnvironmentConnection): Computer {
82
+ return {
83
+ id: environment.id,
84
+ deviceId: environment.deviceId,
85
+ connectionId: environment.connectionId,
86
+ name: environment.connectionName,
87
+ status: environment.connectionId ? "online" : "offline",
88
+ connectedAt: environment.connectedAt,
89
+ lastSeenAt: environment.lastSeenAt,
90
+ metadata: environment.metadata,
91
+ };
92
+ }
93
+
94
+ /** Public Cloud computer discovery and selection client. */
95
+ export class ComputersClientImpl implements ComputersClient {
96
+ private readonly environments: RemoteEnvironmentClient;
97
+
98
+ constructor(client: Letta) {
99
+ this.environments = new RemoteEnvironmentClient({}, client);
100
+ }
101
+
102
+ async list(
103
+ options: ListComputersOptions = {},
104
+ ): Promise<ListComputersResult> {
105
+ const result = await this.environments.listEnvironments(options);
106
+ return {
107
+ computers: result.connections.map(toComputer),
108
+ hasNextPage: result.hasNextPage,
109
+ };
110
+ }
111
+
112
+ async get(deviceId: string): Promise<Computer> {
113
+ if (typeof deviceId !== "string" || deviceId.trim().length === 0) {
114
+ throw new Error("Invalid deviceId. Expected a non-empty string.");
115
+ }
116
+ return toComputer(
117
+ await this.environments.getEnvironmentByDeviceId(deviceId),
118
+ );
119
+ }
120
+
121
+ async resolve(selector: ComputerSelector): Promise<ResolvedComputer> {
122
+ const result = await this.environments.resolveEnvironment(
123
+ selectorToRemoteTarget(selector),
124
+ );
125
+ return {
126
+ connectionId: result.connectionId,
127
+ computer: result.environment
128
+ ? toComputer(result.environment)
129
+ : undefined,
130
+ };
131
+ }
132
+ }
package/src/index.ts CHANGED
@@ -173,6 +173,15 @@ export type {
173
173
  } from "./management-types.js";
174
174
 
175
175
  export { RepositoriesClient } from "./repositories.js";
176
+ export type {
177
+ Computer,
178
+ ComputerMetadata,
179
+ ComputerSelector,
180
+ ComputersClient,
181
+ ListComputersOptions,
182
+ ListComputersResult,
183
+ ResolvedComputer,
184
+ } from "./computers.js";
176
185
  export { LettaAgentClient } from "./client.js";
177
186
  export { CloudManagedSandboxExpiredError } from "./cloud-session.js";
178
187
  export { createReactNativeWebSocketConstructor } from "./websocket.js";
@@ -78,6 +78,9 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
78
78
  private initializePromise: Promise<SDKInitMessage> | null = null;
79
79
  private removeMessageHandler: (() => void) | null = null;
80
80
  private detachTransportDisconnect: (() => void) | null = null;
81
+ private idleTransportDisconnected = false;
82
+ private transportRecoveryPromise: Promise<void> | null = null;
83
+ private transportDisconnectGeneration = 0;
81
84
  private readonly turns: RemoteTurnCoordinator;
82
85
  private toolNames: string[] | undefined;
83
86
  private deviceStatusListeners = new Set<(status: SessionDeviceStatus) => void>();
@@ -207,18 +210,25 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
207
210
  }
208
211
 
209
212
  async send(message: SendMessage): Promise<void> {
213
+ if (this.closed) throw new Error("Session is closed");
210
214
  if (!this.initialized) {
211
215
  await this.initialize();
212
216
  }
217
+ await this.recoverIdleTransportIfNeeded();
213
218
  if (!this.controller || !this.runtime) {
214
219
  throw new Error("Session is not initialized");
215
220
  }
216
221
 
217
222
  await this.beforeTurn();
218
223
 
219
- const turn = this.turns.trackSentTurn(this.runtime);
224
+ const controller = this.controller;
225
+ const runtime = this.runtime;
226
+ if (!controller || !runtime) {
227
+ throw new Error("Session transport disconnected before the turn was sent");
228
+ }
229
+ const turn = this.turns.trackSentTurn(runtime);
220
230
  try {
221
- this.controller.sendTurnMessage(this.runtime, message, {
231
+ controller.sendTurnMessage(runtime, message, {
222
232
  clientMessageId: turn.clientMessageId,
223
233
  });
224
234
  } catch (error) {
@@ -556,6 +566,7 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
556
566
  this.deviceStatusListeners.clear();
557
567
  this.controller?.close();
558
568
  this.controller = null;
569
+ this.idleTransportDisconnected = false;
559
570
  this.onCoreClose();
560
571
  }
561
572
 
@@ -625,27 +636,31 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
625
636
  protected abstract initializeRuntimeController(): Promise<RuntimeSessionInit>;
626
637
 
627
638
  /**
628
- * Fail the session when its transport drops unexpectedly.
629
- *
630
- * Without this an in-flight turn parks forever: nextMessage() only settles
631
- * through the coordinator, and the socket's own pending-request rejection
632
- * covers request/response commands, not streaming turns.
633
- *
634
- * The session is not revived — the runtime lived on the dead socket. The
635
- * caller observes the error and rebuilds via resumeSession().
636
- *
637
- * Subclasses call this only once their runtime is live: a drop before that
638
- * already surfaces as a rejected connect or runtime-start, and failing the
639
- * session there would defeat initialize()'s retry path. Explicit closes do
640
- * not notify, so this fires for faults only. Detaching stays here so every
641
- * teardown path — clean close and failed initialize alike — covers it.
639
+ * Active turns fail because the input may have reached the listener. Cloud
640
+ * sessions may recover an idle connection before the next turn is tracked.
642
641
  */
643
- protected watchTransportDisconnect(client: {
644
- onDisconnect(handler: () => void): () => void;
645
- }): void {
642
+ protected watchTransportDisconnect(
643
+ client: { onDisconnect(handler: () => void): () => void },
644
+ options: { recoverWhenIdle?: boolean } = {},
645
+ ): void {
646
646
  this.detachTransportDisconnect?.();
647
647
  this.detachTransportDisconnect = client.onDisconnect(() => {
648
648
  if (this.closed) return;
649
+ this.transportDisconnectGeneration += 1;
650
+ if (options.recoverWhenIdle && !this.turns.hasInFlightTurn()) {
651
+ this.detachTransportDisconnect?.();
652
+ this.detachTransportDisconnect = null;
653
+ this.removeMessageHandler?.();
654
+ this.removeMessageHandler = null;
655
+ this.controller?.close();
656
+ this.controller = null;
657
+ const error = new Error(`${this.label} connection closed before send`);
658
+ for (const cancel of [...this.deviceStatusRefreshCancels]) cancel(error);
659
+ this.deviceStatusRefreshCancels.clear();
660
+ this.onIdleTransportDisconnect();
661
+ this.idleTransportDisconnected = true;
662
+ return;
663
+ }
649
664
  this.turns.closeWithError(
650
665
  `The ${this.label} connection closed unexpectedly; resume the conversation to continue.`,
651
666
  );
@@ -653,6 +668,62 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
653
668
  });
654
669
  }
655
670
 
671
+ private async recoverIdleTransportIfNeeded(): Promise<void> {
672
+ if (!this.idleTransportDisconnected) return;
673
+ if (this.transportRecoveryPromise) {
674
+ await this.transportRecoveryPromise;
675
+ return;
676
+ }
677
+ const runtime = this.runtime;
678
+ if (!runtime) throw new Error("Session transport disconnected without a runtime");
679
+ const generation = this.transportDisconnectGeneration;
680
+ const recovery = this.recoverIdleTransport(runtime)
681
+ .then(async (init) => {
682
+ if (this.closed || generation !== this.transportDisconnectGeneration) {
683
+ init.controller.close();
684
+ this.onRecoveredTransportDiscarded();
685
+ throw new Error(`${this.label} connection closed during recovery`);
686
+ }
687
+ if (
688
+ init.runtime.agent_id !== runtime.agent_id ||
689
+ init.runtime.conversation_id !== runtime.conversation_id
690
+ ) {
691
+ init.controller.close();
692
+ this.onRecoveredTransportDiscarded();
693
+ throw new Error(`${this.label} transport recovered a different runtime`);
694
+ }
695
+ this.controller = init.controller;
696
+ this.runtime = init.runtime;
697
+ this._modelSettings = init.modelSettings ?? this._modelSettings;
698
+ if (typeof init.model === "string" && init.model) this._model = init.model;
699
+ if (init.tools !== undefined) this.toolNames = init.tools;
700
+ this.removeMessageHandler = this.controller.onMessage((message) => {
701
+ if (this.runtime) this.turns.handleProtocolMessage(message, this.runtime);
702
+ });
703
+ this.idleTransportDisconnected = false;
704
+ await this.afterRuntimeInitialized();
705
+ })
706
+ .finally(() => {
707
+ if (this.transportRecoveryPromise === recovery) this.transportRecoveryPromise = null;
708
+ });
709
+ this.transportRecoveryPromise = recovery;
710
+ await recovery;
711
+ }
712
+
713
+ protected async recoverIdleTransport(
714
+ _runtime: RuntimeScope,
715
+ ): Promise<RuntimeSessionInit> {
716
+ throw new Error(`${this.label} sessions do not support idle transport recovery`);
717
+ }
718
+
719
+ protected onIdleTransportDisconnect(): void {
720
+ // Optional hook for transport-specific handler cleanup before recovery.
721
+ }
722
+
723
+ protected onRecoveredTransportDiscarded(): void {
724
+ // Optional hook when a recovery finishes after the session closed or dropped again.
725
+ }
726
+
656
727
  protected async afterRuntimeInitialized(): Promise<void> {
657
728
  // Optional hook for subclasses to send transport-specific startup frames.
658
729
  }
package/src/remote.ts CHANGED
@@ -35,6 +35,12 @@ export interface RemoteEnvironmentConnection {
35
35
  metadata?: Record<string, unknown>;
36
36
  }
37
37
 
38
+ export interface RemoteEnvironmentListOptions {
39
+ limit?: number;
40
+ after?: string;
41
+ onlineOnly?: boolean;
42
+ }
43
+
38
44
  export interface RemoteEnvironmentListResult {
39
45
  connections: RemoteEnvironmentConnection[];
40
46
  hasNextPage: boolean;
@@ -59,7 +65,7 @@ function ensureOnline(
59
65
  : "connectionName" in target
60
66
  ? target.connectionName
61
67
  : environment.deviceId;
62
- throw new Error(`Remote environment is offline: ${label}`);
68
+ throw new Error(`Computer is offline: ${label}`);
63
69
  }
64
70
 
65
71
  return {
@@ -84,8 +90,16 @@ export class RemoteEnvironmentClient {
84
90
  }),
85
91
  ) {}
86
92
 
87
- async listEnvironments(): Promise<RemoteEnvironmentListResult> {
88
- return await this.client.environments.list() as RemoteEnvironmentListResult;
93
+ async listEnvironments(
94
+ options: RemoteEnvironmentListOptions = {},
95
+ ): Promise<RemoteEnvironmentListResult> {
96
+ return await this.client.environments.list({
97
+ after: options.after,
98
+ limit: options.limit === undefined ? undefined : String(options.limit),
99
+ onlineOnly: options.onlineOnly === undefined
100
+ ? undefined
101
+ : String(options.onlineOnly),
102
+ }) as RemoteEnvironmentListResult;
89
103
  }
90
104
 
91
105
  async getEnvironmentByDeviceId(deviceId: string): Promise<RemoteEnvironmentConnection> {
@@ -103,7 +117,15 @@ export class RemoteEnvironmentClient {
103
117
  return ensureOnline(await this.getEnvironmentByDeviceId(target.deviceId), target);
104
118
  }
105
119
 
106
- const { connections } = await this.listEnvironments();
120
+ const connections: RemoteEnvironmentConnection[] = [];
121
+ let after: string | undefined;
122
+ do {
123
+ const page = await this.listEnvironments({ limit: 100, after });
124
+ connections.push(...page.connections);
125
+ if (!page.hasNextPage || page.connections.length === 0) break;
126
+ after = page.connections.at(-1)?.id;
127
+ } while (after !== undefined);
128
+
107
129
  if ("environmentId" in target) {
108
130
  const match = connections.find((env) => env.id === target.environmentId);
109
131
  if (!match) {
@@ -116,17 +138,24 @@ export class RemoteEnvironmentClient {
116
138
  (env) => env.connectionName === target.connectionName,
117
139
  );
118
140
  if (matches.length === 0) {
119
- throw new Error(`Remote environment not found: ${target.connectionName}`);
141
+ throw new Error(`Computer not found: ${target.connectionName}`);
120
142
  }
121
- if (matches.length > 1) {
143
+ const onlineMatches = matches.filter(
144
+ (environment) => environment.connectionId !== null,
145
+ );
146
+ if (onlineMatches.length === 0) {
147
+ throw new Error(`Computer is offline: ${target.connectionName}`);
148
+ }
149
+ if (onlineMatches.length > 1) {
150
+ const candidates = onlineMatches
151
+ .map((environment) =>
152
+ `${environment.connectionName} (${environment.deviceId}, online)`
153
+ )
154
+ .join(", ");
122
155
  throw new Error(
123
- `Remote environment name is ambiguous: ${target.connectionName}`,
156
+ `Computer name is ambiguous: ${target.connectionName}. Matches: ${candidates}. Select one by deviceId.`,
124
157
  );
125
158
  }
126
- const match = matches[0];
127
- if (!match) {
128
- throw new Error(`Remote environment not found: ${target.connectionName}`);
129
- }
130
- return ensureOnline(match, target);
159
+ return ensureOnline(onlineMatches[0]!, target);
131
160
  }
132
161
  }
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@ import type {
17
17
  import type { Message as LettaMessage } from "@letta-ai/letta-client/resources/agents/messages";
18
18
  import type { CreateBlock } from "@letta-ai/letta-client/resources/blocks/blocks";
19
19
  import type { LettaCodeCloudSandboxOptions } from "./cloud-sandbox.js";
20
+ import type { ComputerSelector } from "./computers.js";
20
21
  export type { CreateBlock } from "@letta-ai/letta-client/resources/blocks/blocks";
21
22
  export type {
22
23
  GitHubRepositoryRef,
@@ -301,18 +302,8 @@ export type McpServers = Record<string, McpServerConfig>;
301
302
  */
302
303
  export type LettaCodeBackend = "local" | "remote" | "cloud";
303
304
 
304
- /**
305
- * Stable execution target for remote/cloud runtimes.
306
- *
307
- * Strings are treated as human-readable environment names. Object forms allow
308
- * callers to avoid relying on names as unique identifiers.
309
- */
310
- export type LettaCodeEnvironment =
311
- | string
312
- | { name: string }
313
- | { id: string }
314
- | { connectionId: string }
315
- | { deviceId: string };
305
+ /** @deprecated Use `ComputerSelector` from the package root. */
306
+ export type LettaCodeEnvironment = ComputerSelector;
316
307
 
317
308
  export interface LettaCodeLocalAppServerOptions {
318
309
  /**
@@ -383,11 +374,13 @@ export interface LettaCodeCloudClientOptions {
383
374
  /** Heartbeat interval for the Cloud status websocket. Defaults to 30s. */
384
375
  pingIntervalMs?: number;
385
376
  /**
386
- * Execution target for Letta Cloud sessions. If omitted, the SDK creates
387
- * and owns a sandbox for the session.
377
+ * Computer where Letta Cloud sessions run. If omitted, the SDK creates and
378
+ * owns a managed sandbox for the session.
388
379
  */
380
+ computer?: ComputerSelector;
381
+ /** @deprecated Use `computer`. */
389
382
  environment?: LettaCodeEnvironment;
390
- /** Options for SDK-managed sandboxes when environment is omitted. */
383
+ /** Options for SDK-managed sandboxes when no computer is selected. */
391
384
  sandbox?: LettaCodeCloudSandboxOptions;
392
385
  }
393
386
 
@@ -709,12 +702,14 @@ export interface CreateSessionOptions {
709
702
  /**
710
703
  * Session options accepted by LettaAgentClient methods.
711
704
  *
712
- * `environment` is a cloud execution-target override. It is deliberately
705
+ * `computer` is a Cloud execution-target override. It is deliberately
713
706
  * session-scoped rather than part of createAgent() options.
714
707
  */
715
708
  export interface LettaCodeClientSessionOptions extends CreateSessionOptions {
709
+ computer?: ComputerSelector;
710
+ /** @deprecated Use `computer`. */
716
711
  environment?: LettaCodeEnvironment;
717
- /** Per-session SDK-managed sandbox options when environment is omitted. */
712
+ /** Per-session SDK-managed sandbox options when no computer is selected. */
718
713
  sandbox?: LettaCodeCloudSandboxOptions;
719
714
  /**
720
715
  * Extra environment variables for the session's harness process. Each