@letta-ai/letta-agent-sdk 0.3.3 → 0.5.1
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/AGENTS.md +0 -3
- package/README.md +8 -1
- package/dist/app-server-management.d.ts +3 -48
- package/dist/app-server-management.d.ts.map +1 -1
- package/dist/app-server-session.d.ts +0 -6
- package/dist/app-server-session.d.ts.map +1 -1
- package/dist/client-base.d.ts +7 -15
- package/dist/client-base.d.ts.map +1 -1
- package/dist/client-entry.js +428 -486
- package/dist/client-entry.js.map +13 -12
- package/dist/client.d.ts +3 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/cloud-session.d.ts +2 -0
- package/dist/cloud-session.d.ts.map +1 -1
- package/dist/cloud-status-transport.d.ts +53 -0
- package/dist/cloud-status-transport.d.ts.map +1 -0
- package/dist/index.d.ts +7 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +911 -1993
- package/dist/index.js.map +18 -18
- package/dist/local-app-server-session.d.ts +1 -1
- package/dist/local-app-server-session.d.ts.map +1 -1
- package/dist/local-app-server.d.ts +11 -0
- package/dist/local-app-server.d.ts.map +1 -1
- package/dist/management.d.ts +0 -7
- package/dist/management.d.ts.map +1 -1
- package/dist/remote-session-protocol.d.ts +2 -0
- package/dist/remote-session-protocol.d.ts.map +1 -1
- package/dist/remote-turn-coordinator.d.ts.map +1 -1
- package/dist/types.d.ts +65 -66
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/app-server-management.ts +20 -256
- package/src/app-server-session.ts +0 -32
- package/src/client-base.ts +47 -73
- package/src/client.ts +31 -70
- package/src/cloud-session.ts +113 -287
- package/src/cloud-status-transport.ts +305 -0
- package/src/index.ts +11 -29
- package/src/local-app-server-session.ts +13 -3
- package/src/local-app-server.ts +97 -3
- package/src/management.ts +0 -7
- package/src/remote-session-protocol.ts +18 -0
- package/src/remote-turn-coordinator.ts +20 -2
- package/src/types.ts +72 -111
- package/src/validation.ts +21 -6
- package/dist/protocol.d.ts +0 -205
- package/dist/protocol.d.ts.map +0 -1
- package/dist/session.d.ts +0 -155
- package/dist/session.d.ts.map +0 -1
- package/dist/transport.d.ts +0 -53
- package/dist/transport.d.ts.map +0 -1
- package/src/protocol.ts +0 -249
- package/src/session.ts +0 -1638
- package/src/transport.ts +0 -484
package/src/cloud-session.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createAppServerClient,
|
|
3
3
|
type AppServerClient,
|
|
4
|
-
type AppServerSocketConstructor,
|
|
5
|
-
type AppServerSocketLike,
|
|
6
|
-
type AppServerSocketOptions,
|
|
7
4
|
} from "@letta-ai/letta-code/app-server-client";
|
|
8
5
|
import {
|
|
9
6
|
AppServerRuntimeController,
|
|
@@ -13,6 +10,7 @@ import {
|
|
|
13
10
|
externalToolGroups,
|
|
14
11
|
registerAppServerControlRequestHandler,
|
|
15
12
|
} from "./app-server-session.js";
|
|
13
|
+
import { createCloudStatusTransportConstructor } from "./cloud-status-transport.js";
|
|
16
14
|
import {
|
|
17
15
|
RemoteEnvironmentClient,
|
|
18
16
|
type RemoteEnvironmentTarget,
|
|
@@ -33,7 +31,6 @@ import type {
|
|
|
33
31
|
LettaCodeCloudClientOptions,
|
|
34
32
|
LettaCodeEnvironment,
|
|
35
33
|
LettaCodeSocketConstructor,
|
|
36
|
-
LettaCodeSocketLike,
|
|
37
34
|
RepositoryResource,
|
|
38
35
|
} from "./types.js";
|
|
39
36
|
import {
|
|
@@ -53,24 +50,6 @@ const SDK_AGENT_ORIGIN = "@letta-ai/letta-agent-sdk";
|
|
|
53
50
|
|
|
54
51
|
type FetchLike = typeof fetch;
|
|
55
52
|
|
|
56
|
-
type CloudStatusMessage = ProtocolMessage & {
|
|
57
|
-
type: string;
|
|
58
|
-
seq?: unknown;
|
|
59
|
-
event_seq?: unknown;
|
|
60
|
-
idempotency_key?: unknown;
|
|
61
|
-
runId?: unknown;
|
|
62
|
-
run_id?: unknown;
|
|
63
|
-
stopReason?: unknown;
|
|
64
|
-
stop_reason?: unknown;
|
|
65
|
-
conversation_id?: unknown;
|
|
66
|
-
conversationId?: unknown;
|
|
67
|
-
agent_id?: unknown;
|
|
68
|
-
agentId?: unknown;
|
|
69
|
-
request?: unknown;
|
|
70
|
-
loop_status?: unknown;
|
|
71
|
-
delta?: unknown;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
53
|
type CloudRuntimeStartResponse = ProtocolMessage & {
|
|
75
54
|
type: "runtime_start_response";
|
|
76
55
|
success: boolean;
|
|
@@ -328,231 +307,6 @@ function buildCloudStatusWebSocketUrl(params: {
|
|
|
328
307
|
return base.toString();
|
|
329
308
|
}
|
|
330
309
|
|
|
331
|
-
function addSocketListener(
|
|
332
|
-
socket: LettaCodeSocketLike,
|
|
333
|
-
type: string,
|
|
334
|
-
listener: (event: unknown) => void,
|
|
335
|
-
): () => void {
|
|
336
|
-
if (socket.addEventListener) {
|
|
337
|
-
socket.addEventListener(type, listener);
|
|
338
|
-
return () => socket.removeEventListener?.(type, listener);
|
|
339
|
-
}
|
|
340
|
-
if (socket.on) {
|
|
341
|
-
socket.on(type, listener);
|
|
342
|
-
return () => socket.off?.(type, listener);
|
|
343
|
-
}
|
|
344
|
-
throw new Error("WebSocket implementation does not support event listeners.");
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function messageEventData(event: unknown): string | null {
|
|
348
|
-
if (typeof event === "string") return event;
|
|
349
|
-
if (event && typeof event === "object") {
|
|
350
|
-
const data = (event as { data?: unknown }).data;
|
|
351
|
-
if (typeof data === "string") return data;
|
|
352
|
-
if (data instanceof ArrayBuffer) {
|
|
353
|
-
return new TextDecoder().decode(data);
|
|
354
|
-
}
|
|
355
|
-
if (data instanceof Uint8Array) {
|
|
356
|
-
return new TextDecoder().decode(data);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
type CloudStatusSocketControl = AppServerSocketLike & {
|
|
363
|
-
sendCloudCommand(command: Record<string, unknown>): void;
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
type CloudStatusSocketState = {
|
|
367
|
-
runtime: RuntimeScope;
|
|
368
|
-
seenIdempotencyKeys: Set<string>;
|
|
369
|
-
seenIdempotencyOrder: string[];
|
|
370
|
-
lastEventSeq: number | null;
|
|
371
|
-
controlSocket: CloudStatusSocketControl | null;
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
function createCloudStatusWebSocketConstructor(params: {
|
|
375
|
-
cloudOptions: LettaCodeCloudClientOptions;
|
|
376
|
-
runtime: RuntimeScope;
|
|
377
|
-
}): AppServerSocketConstructor {
|
|
378
|
-
const WebSocketCtor = getWebSocketConstructor(params.cloudOptions.WebSocket);
|
|
379
|
-
const authMode = params.cloudOptions.webSocketAuth ?? "header";
|
|
380
|
-
const cloudHeaders = authMode === "header" ? cloudWebSocketHeaders(params.cloudOptions) : undefined;
|
|
381
|
-
const pingIntervalMs = params.cloudOptions.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
|
|
382
|
-
const state: CloudStatusSocketState = {
|
|
383
|
-
runtime: params.runtime,
|
|
384
|
-
seenIdempotencyKeys: new Set<string>(),
|
|
385
|
-
seenIdempotencyOrder: [],
|
|
386
|
-
lastEventSeq: null,
|
|
387
|
-
controlSocket: null,
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
class CloudStatusSocketAdapter implements AppServerSocketLike {
|
|
391
|
-
private readonly socket: LettaCodeSocketLike;
|
|
392
|
-
private readonly channel: string | null;
|
|
393
|
-
private readonly listenerRemovers = new Map<string, Map<(event: unknown) => void, () => void>>();
|
|
394
|
-
private pingTimer: ReturnType<typeof setInterval> | null = null;
|
|
395
|
-
private removeCloseListener: (() => void) | null = null;
|
|
396
|
-
|
|
397
|
-
constructor(url: string, options?: AppServerSocketOptions) {
|
|
398
|
-
this.channel = new URL(url).searchParams.get("channel");
|
|
399
|
-
const mergedHeaders = authMode === "header"
|
|
400
|
-
? {
|
|
401
|
-
...(options?.headers ?? {}),
|
|
402
|
-
...(cloudHeaders ?? {}),
|
|
403
|
-
}
|
|
404
|
-
: undefined;
|
|
405
|
-
const socketOptions = mergedHeaders && Object.keys(mergedHeaders).length > 0
|
|
406
|
-
? { headers: mergedHeaders }
|
|
407
|
-
: undefined;
|
|
408
|
-
this.socket = new WebSocketCtor(url, socketOptions);
|
|
409
|
-
if (this.channel === "control") {
|
|
410
|
-
state.controlSocket = this;
|
|
411
|
-
}
|
|
412
|
-
this.removeCloseListener = addSocketListener(this.socket, "close", () => {
|
|
413
|
-
this.stopPing();
|
|
414
|
-
if (state.controlSocket === this) state.controlSocket = null;
|
|
415
|
-
});
|
|
416
|
-
this.startPing(pingIntervalMs);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
get readyState(): number {
|
|
420
|
-
return this.socket.readyState;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
send(data: string): void {
|
|
424
|
-
this.socket.send(data);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
close(): void {
|
|
428
|
-
this.stopPing();
|
|
429
|
-
this.removeCloseListener?.();
|
|
430
|
-
this.removeCloseListener = null;
|
|
431
|
-
if (state.controlSocket === this) state.controlSocket = null;
|
|
432
|
-
this.socket.close();
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
436
|
-
const wrapped = type === "message"
|
|
437
|
-
? (event: unknown) => {
|
|
438
|
-
if (this.handleIncomingMessage(event)) {
|
|
439
|
-
listener(event);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
: listener;
|
|
443
|
-
const remove = addSocketListener(this.socket, type, wrapped);
|
|
444
|
-
let typeRemovers = this.listenerRemovers.get(type);
|
|
445
|
-
if (!typeRemovers) {
|
|
446
|
-
typeRemovers = new Map();
|
|
447
|
-
this.listenerRemovers.set(type, typeRemovers);
|
|
448
|
-
}
|
|
449
|
-
typeRemovers.set(listener, remove);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
453
|
-
const typeRemovers = this.listenerRemovers.get(type);
|
|
454
|
-
const remove = typeRemovers?.get(listener);
|
|
455
|
-
if (!remove) return;
|
|
456
|
-
remove();
|
|
457
|
-
typeRemovers?.delete(listener);
|
|
458
|
-
if (typeRemovers?.size === 0) {
|
|
459
|
-
this.listenerRemovers.delete(type);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
private handleIncomingMessage(event: unknown): boolean {
|
|
464
|
-
const data = messageEventData(event);
|
|
465
|
-
if (!data) return true;
|
|
466
|
-
|
|
467
|
-
let message: CloudStatusMessage;
|
|
468
|
-
try {
|
|
469
|
-
message = JSON.parse(data) as CloudStatusMessage;
|
|
470
|
-
} catch {
|
|
471
|
-
return true;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
this.ackIfSequenced(message);
|
|
475
|
-
|
|
476
|
-
// The status gateway can mirror device stream frames to the control
|
|
477
|
-
// subscriber. Drop them at the Cloud transport boundary instead of making
|
|
478
|
-
// shared app-server session code understand Cloud fanout quirks. Do this
|
|
479
|
-
// before idempotency/event tracking so the canonical stream-channel frame
|
|
480
|
-
// is still delivered and sequenced.
|
|
481
|
-
if (this.channel === "control" && message.type === "stream_delta") {
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (this.isDuplicate(message)) return false;
|
|
486
|
-
this.trackEventSeq(message);
|
|
487
|
-
return true;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
private ackIfSequenced(message: CloudStatusMessage): void {
|
|
491
|
-
if (typeof message.seq !== "number") return;
|
|
492
|
-
this.sendCloudCommand({ type: "ack", seq: message.seq });
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
private isDuplicate(message: CloudStatusMessage): boolean {
|
|
496
|
-
const idempotencyKey =
|
|
497
|
-
typeof message.idempotency_key === "string" ? message.idempotency_key : undefined;
|
|
498
|
-
if (!idempotencyKey) return false;
|
|
499
|
-
if (state.seenIdempotencyKeys.has(idempotencyKey)) return true;
|
|
500
|
-
state.seenIdempotencyKeys.add(idempotencyKey);
|
|
501
|
-
state.seenIdempotencyOrder.push(idempotencyKey);
|
|
502
|
-
while (state.seenIdempotencyOrder.length > 1_000) {
|
|
503
|
-
const oldest = state.seenIdempotencyOrder.shift();
|
|
504
|
-
if (oldest) state.seenIdempotencyKeys.delete(oldest);
|
|
505
|
-
}
|
|
506
|
-
return false;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
private trackEventSeq(message: CloudStatusMessage): void {
|
|
510
|
-
if (typeof message.event_seq !== "number") return;
|
|
511
|
-
if (state.lastEventSeq !== null && message.event_seq > state.lastEventSeq + 1) {
|
|
512
|
-
this.sendSync(true);
|
|
513
|
-
}
|
|
514
|
-
if (state.lastEventSeq === null || message.event_seq > state.lastEventSeq) {
|
|
515
|
-
state.lastEventSeq = message.event_seq;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
private sendSync(recoverApprovals: boolean): void {
|
|
520
|
-
const target = state.controlSocket?.readyState === 1 ? state.controlSocket : this;
|
|
521
|
-
target.sendCloudCommand({
|
|
522
|
-
type: "sync",
|
|
523
|
-
runtime: state.runtime,
|
|
524
|
-
recover_approvals: recoverApprovals,
|
|
525
|
-
force_device_status: true,
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
sendCloudCommand(command: Record<string, unknown>): void {
|
|
530
|
-
if (this.readyState !== 1) return;
|
|
531
|
-
try {
|
|
532
|
-
this.socket.send(JSON.stringify(command));
|
|
533
|
-
} catch {
|
|
534
|
-
// Best-effort Cloud status reliability command.
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
private startPing(intervalMs: number): void {
|
|
539
|
-
if (this.pingTimer) return;
|
|
540
|
-
this.pingTimer = setInterval(() => {
|
|
541
|
-
this.sendCloudCommand({ type: "ping" });
|
|
542
|
-
}, intervalMs);
|
|
543
|
-
(this.pingTimer as { unref?: () => void }).unref?.();
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
private stopPing(): void {
|
|
547
|
-
if (!this.pingTimer) return;
|
|
548
|
-
clearInterval(this.pingTimer);
|
|
549
|
-
this.pingTimer = null;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return CloudStatusSocketAdapter as AppServerSocketConstructor;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
310
|
function isCloudConversation(value: unknown): value is CloudConversation & { id: string } {
|
|
557
311
|
return Boolean(value && typeof value === "object" && typeof (value as CloudConversation).id === "string");
|
|
558
312
|
}
|
|
@@ -638,24 +392,6 @@ export function assertCloudSessionOptionsSupported(
|
|
|
638
392
|
if (options.environment !== undefined && options.sandbox !== undefined) {
|
|
639
393
|
throw new Error(`Letta Cloud ${action}() cannot specify both environment and sandbox options.`);
|
|
640
394
|
}
|
|
641
|
-
if (options.systemPrompt !== undefined) {
|
|
642
|
-
throw new Error(`Letta Cloud ${action}() cannot rewrite an existing agent's systemPrompt from the SDK adapter yet.`);
|
|
643
|
-
}
|
|
644
|
-
if (options.disallowedTools !== undefined) {
|
|
645
|
-
throw new Error(`Letta Cloud ${action}() has not wired disallowedTools to the remote device protocol yet.`);
|
|
646
|
-
}
|
|
647
|
-
if (options.systemInfoReminder !== undefined) {
|
|
648
|
-
throw new Error(`Letta Cloud ${action}() has not wired systemInfoReminder to the remote device protocol yet.`);
|
|
649
|
-
}
|
|
650
|
-
if (options.dreaming?.behavior !== undefined) {
|
|
651
|
-
throw new Error(`Letta Cloud ${action}() does not yet support dreaming.behavior overrides.`);
|
|
652
|
-
}
|
|
653
|
-
if ((options as { memfsStartup?: unknown }).memfsStartup !== undefined) {
|
|
654
|
-
throw new Error(`Letta Cloud ${action}() does not support memfsStartup.`);
|
|
655
|
-
}
|
|
656
|
-
if (options.includePartialMessages !== undefined) {
|
|
657
|
-
throw new Error(`Letta Cloud ${action}() streams Remote Client deltas directly; includePartialMessages is not a separate toggle.`);
|
|
658
|
-
}
|
|
659
395
|
}
|
|
660
396
|
|
|
661
397
|
export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
@@ -668,6 +404,7 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
668
404
|
private sandboxRefreshInFlight: Promise<void> | null = null;
|
|
669
405
|
private sandboxLifecycleClosing = false;
|
|
670
406
|
private attachedRepositoryIds = new Set<string>();
|
|
407
|
+
private repositoryIdsRequiringCleanupRecompile = new Set<string>();
|
|
671
408
|
private readonly cloudMode: CloudSessionMode;
|
|
672
409
|
|
|
673
410
|
constructor(
|
|
@@ -685,7 +422,15 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
685
422
|
|
|
686
423
|
protected override async initializeRuntimeController(): Promise<RuntimeSessionInit> {
|
|
687
424
|
const resolved = await this.resolveRuntime();
|
|
688
|
-
const connection = await this.resolveConnectionForRuntime(resolved.runtime)
|
|
425
|
+
const connection = await this.resolveConnectionForRuntime(resolved.runtime).catch(
|
|
426
|
+
async (error: unknown) => {
|
|
427
|
+
await this.cleanupSessionRepositories(
|
|
428
|
+
resolved.runtime.agent_id,
|
|
429
|
+
resolved.runtime.conversation_id,
|
|
430
|
+
);
|
|
431
|
+
throw error;
|
|
432
|
+
},
|
|
433
|
+
);
|
|
689
434
|
this.connectionId = connection.connectionId;
|
|
690
435
|
|
|
691
436
|
const apiKey = getCloudApiKey(this.cloudOptions);
|
|
@@ -699,8 +444,14 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
699
444
|
});
|
|
700
445
|
const client = applyUniqueRequestIds(createAppServerClient({
|
|
701
446
|
url,
|
|
702
|
-
WebSocket:
|
|
703
|
-
|
|
447
|
+
WebSocket: createCloudStatusTransportConstructor({
|
|
448
|
+
url,
|
|
449
|
+
WebSocket: getWebSocketConstructor(this.cloudOptions.WebSocket),
|
|
450
|
+
...((this.cloudOptions.webSocketAuth ?? "header") === "header"
|
|
451
|
+
? { headers: cloudWebSocketHeaders(this.cloudOptions) }
|
|
452
|
+
: {}),
|
|
453
|
+
pingIntervalMs:
|
|
454
|
+
this.cloudOptions.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS,
|
|
704
455
|
runtime: resolved.runtime,
|
|
705
456
|
}),
|
|
706
457
|
requestTimeoutMs: this.cloudOptions.requestTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS,
|
|
@@ -746,7 +497,10 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
746
497
|
this.removeControlRequestHandler = null;
|
|
747
498
|
client.close();
|
|
748
499
|
await this.cleanupManagedSandbox();
|
|
749
|
-
await this.cleanupSessionRepositories(
|
|
500
|
+
await this.cleanupSessionRepositories(
|
|
501
|
+
resolved.runtime.agent_id,
|
|
502
|
+
resolved.runtime.conversation_id,
|
|
503
|
+
);
|
|
750
504
|
throw error;
|
|
751
505
|
}
|
|
752
506
|
}
|
|
@@ -803,7 +557,12 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
803
557
|
this.removeControlRequestHandler?.();
|
|
804
558
|
this.removeControlRequestHandler = null;
|
|
805
559
|
void this.cleanupManagedSandbox();
|
|
806
|
-
if (this.runtime?.agent_id)
|
|
560
|
+
if (this.runtime?.agent_id) {
|
|
561
|
+
void this.cleanupSessionRepositories(
|
|
562
|
+
this.runtime.agent_id,
|
|
563
|
+
this.runtime.conversation_id,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
807
566
|
}
|
|
808
567
|
|
|
809
568
|
private async resolveRuntime(): Promise<{ runtime: RuntimeScope }> {
|
|
@@ -824,27 +583,37 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
824
583
|
);
|
|
825
584
|
}
|
|
826
585
|
|
|
827
|
-
|
|
586
|
+
const shouldRecompileRepositories =
|
|
587
|
+
await this.attachSessionRepositories(agentId);
|
|
828
588
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
589
|
+
try {
|
|
590
|
+
if (this.cloudMode.newConversation) {
|
|
591
|
+
const conversation = await this.createConversation(agentId);
|
|
592
|
+
conversationId = conversation.id;
|
|
593
|
+
} else if (this.cloudMode.defaultConversation) {
|
|
594
|
+
conversationId = "default";
|
|
595
|
+
}
|
|
835
596
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
597
|
+
if (!conversationId) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
"Letta Cloud createSession()/resumeSession() requires an agent id or conversation id.",
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (shouldRecompileRepositories) {
|
|
604
|
+
await this.recompileSystemPrompt(agentId, conversationId);
|
|
605
|
+
}
|
|
841
606
|
|
|
842
|
-
|
|
607
|
+
return { runtime: { agent_id: agentId, conversation_id: conversationId } };
|
|
608
|
+
} catch (error) {
|
|
609
|
+
await this.cleanupSessionRepositories(agentId, conversationId);
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
843
612
|
}
|
|
844
613
|
|
|
845
|
-
private async attachSessionRepositories(agentId: string): Promise<
|
|
614
|
+
private async attachSessionRepositories(agentId: string): Promise<boolean> {
|
|
846
615
|
const resources = this.repositoryResources();
|
|
847
|
-
if (resources.length === 0) return;
|
|
616
|
+
if (resources.length === 0) return false;
|
|
848
617
|
|
|
849
618
|
const existing = await this.listAgentRepositories(agentId);
|
|
850
619
|
const existingIds = new Set(existing.map((repository) => repository.id));
|
|
@@ -862,18 +631,75 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
862
631
|
await this.cleanupSessionRepositories(agentId);
|
|
863
632
|
throw error;
|
|
864
633
|
}
|
|
634
|
+
|
|
635
|
+
// Recompile even when every requested repository was already linked: the
|
|
636
|
+
// target conversation may still have been compiled before those links
|
|
637
|
+
// existed. One recompile after the full batch produces the desired state.
|
|
638
|
+
const shouldRecompile = resources.some(
|
|
639
|
+
(resource) => resource.recompile !== false,
|
|
640
|
+
);
|
|
641
|
+
this.repositoryIdsRequiringCleanupRecompile = new Set(
|
|
642
|
+
resources
|
|
643
|
+
.filter(
|
|
644
|
+
(resource) =>
|
|
645
|
+
resource.recompile !== false &&
|
|
646
|
+
this.attachedRepositoryIds.has(resource.repositoryId),
|
|
647
|
+
)
|
|
648
|
+
.map((resource) => resource.repositoryId),
|
|
649
|
+
);
|
|
650
|
+
return shouldRecompile;
|
|
865
651
|
}
|
|
866
652
|
|
|
867
|
-
private async cleanupSessionRepositories(
|
|
653
|
+
private async cleanupSessionRepositories(
|
|
654
|
+
agentId: string,
|
|
655
|
+
conversationId?: string,
|
|
656
|
+
): Promise<void> {
|
|
868
657
|
const repositoryIds = [...this.attachedRepositoryIds];
|
|
658
|
+
const repositoryIdsRequiringRecompile =
|
|
659
|
+
this.repositoryIdsRequiringCleanupRecompile;
|
|
869
660
|
this.attachedRepositoryIds.clear();
|
|
870
|
-
|
|
661
|
+
this.repositoryIdsRequiringCleanupRecompile = new Set<string>();
|
|
662
|
+
const recompileAfterDetach = await Promise.all(repositoryIds.map(async (repositoryId) => {
|
|
871
663
|
try {
|
|
872
664
|
await this.unlinkAgentRepository(agentId, repositoryId);
|
|
665
|
+
return repositoryIdsRequiringRecompile.has(repositoryId);
|
|
873
666
|
} catch {
|
|
874
667
|
// Best-effort cleanup: only repositories this SDK session attached are removed.
|
|
668
|
+
return false;
|
|
875
669
|
}
|
|
876
670
|
}));
|
|
671
|
+
// Best-effort recompile after detach so remaining conversations drop stale
|
|
672
|
+
// repository projections. Errors are swallowed because this runs on close.
|
|
673
|
+
if (recompileAfterDetach.some(Boolean)) {
|
|
674
|
+
try {
|
|
675
|
+
await this.recompileSystemPrompt(agentId, conversationId);
|
|
676
|
+
} catch {
|
|
677
|
+
// Best-effort: cleanup must not throw.
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private async recompileSystemPrompt(
|
|
683
|
+
agentId: string,
|
|
684
|
+
conversationId?: string,
|
|
685
|
+
): Promise<void> {
|
|
686
|
+
const fetchImpl = getFetch(this.cloudOptions.fetch);
|
|
687
|
+
const baseUrl = normalizeCloudApiBaseUrl(this.cloudOptions.apiBaseUrl);
|
|
688
|
+
const usesAgentPrompt = !conversationId || conversationId === "default";
|
|
689
|
+
const path = usesAgentPrompt
|
|
690
|
+
? `/v1/agents/${encodeURIComponent(agentId)}/recompile`
|
|
691
|
+
: `/v1/conversations/${encodeURIComponent(conversationId)}/recompile`;
|
|
692
|
+
const url = `${baseUrl}${path}`;
|
|
693
|
+
const response = await fetchImpl(
|
|
694
|
+
url,
|
|
695
|
+
{
|
|
696
|
+
method: "POST",
|
|
697
|
+
headers: cloudHeaders(this.cloudOptions),
|
|
698
|
+
body: JSON.stringify(usesAgentPrompt ? {} : { agent_id: agentId }),
|
|
699
|
+
},
|
|
700
|
+
);
|
|
701
|
+
const body = await parseJsonResponse(response);
|
|
702
|
+
assertOkResponse(response, body, "Cloud recompile system prompt", url);
|
|
877
703
|
}
|
|
878
704
|
|
|
879
705
|
private repositoryResources(): RepositoryResource[] {
|