@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.
- package/README.md +37 -1
- package/dist/client-base.d.ts +7 -0
- package/dist/client-base.d.ts.map +1 -1
- package/dist/client-entry.d.ts +1 -0
- package/dist/client-entry.d.ts.map +1 -1
- package/dist/client-entry.js +323 -118
- package/dist/client-entry.js.map +9 -8
- package/dist/cloud-session.d.ts +8 -1
- package/dist/cloud-session.d.ts.map +1 -1
- package/dist/computers.d.ts +67 -0
- package/dist/computers.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +323 -118
- package/dist/index.js.map +9 -8
- package/dist/remote-client-session-core.d.ts +11 -14
- package/dist/remote-client-session-core.d.ts.map +1 -1
- package/dist/remote.d.ts +6 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/types.d.ts +12 -20
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client-base.ts +66 -19
- package/src/client-entry.ts +9 -0
- package/src/cloud-session.ts +111 -45
- package/src/computers.ts +132 -0
- package/src/index.ts +9 -0
- package/src/remote-client-session-core.ts +90 -19
- package/src/remote.ts +41 -12
- package/src/types.ts +12 -17
package/src/cloud-session.ts
CHANGED
|
@@ -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.
|
|
175
|
-
throw new Error(
|
|
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
|
|
353
|
-
agentId:
|
|
354
|
-
conversationId:
|
|
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
|
|
411
|
+
runtime,
|
|
369
412
|
}),
|
|
370
413
|
requestTimeoutMs: this.cloudOptions.requestTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS,
|
|
371
414
|
}));
|
|
372
415
|
const options = this.currentOptions();
|
|
373
|
-
this.
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
1052
|
+
return modeComputer ??
|
|
1053
|
+
this.cloudOptions.computer ??
|
|
1054
|
+
this.cloudOptions.environment;
|
|
989
1055
|
}
|
|
990
1056
|
|
|
991
1057
|
private effectiveSandboxOptions(): LettaCodeCloudSandboxOptions | undefined {
|
package/src/computers.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
*
|
|
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(
|
|
644
|
-
onDisconnect(handler: () => void): () => void
|
|
645
|
-
|
|
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(`
|
|
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(
|
|
88
|
-
|
|
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
|
|
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(`
|
|
141
|
+
throw new Error(`Computer not found: ${target.connectionName}`);
|
|
120
142
|
}
|
|
121
|
-
|
|
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
|
-
`
|
|
156
|
+
`Computer name is ambiguous: ${target.connectionName}. Matches: ${candidates}. Select one by deviceId.`,
|
|
124
157
|
);
|
|
125
158
|
}
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
387
|
-
*
|
|
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
|
|
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
|
-
* `
|
|
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
|
|
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
|