@letta-ai/letta-agent-sdk 0.6.2 → 0.7.0
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 +42 -0
- package/README.md +101 -1
- package/dist/app-server-session.d.ts.map +1 -1
- package/dist/client-base.d.ts +7 -0
- package/dist/client-base.d.ts.map +1 -1
- package/dist/client-entry.d.ts +3 -0
- package/dist/client-entry.d.ts.map +1 -1
- package/dist/client-entry.js +816 -138
- package/dist/client-entry.js.map +16 -14
- 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 +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +817 -139
- package/dist/index.js.map +17 -15
- package/dist/remote-client-session-core.d.ts +13 -16
- package/dist/remote-client-session-core.d.ts.map +1 -1
- package/dist/remote-session-protocol.d.ts +11 -0
- package/dist/remote-session-protocol.d.ts.map +1 -1
- package/dist/remote-turn-coordinator.d.ts +6 -1
- package/dist/remote-turn-coordinator.d.ts.map +1 -1
- package/dist/remote.d.ts +6 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/transcript-accumulator.d.ts +133 -0
- package/dist/transcript-accumulator.d.ts.map +1 -0
- package/dist/types.d.ts +41 -21
- 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-session.ts +9 -0
- package/src/client-base.ts +66 -19
- package/src/client-entry.ts +23 -0
- package/src/cloud-session.ts +112 -45
- package/src/computers.ts +132 -0
- package/src/index.ts +24 -0
- package/src/remote-client-session-core.ts +103 -27
- package/src/remote-session-protocol.ts +24 -0
- package/src/remote-turn-coordinator.ts +11 -2
- package/src/remote.ts +41 -12
- package/src/transcript-accumulator.ts +823 -0
- package/src/types.ts +43 -18
- package/src/validation.ts +16 -0
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
SDKResultMessage,
|
|
21
21
|
SendCommandOptions,
|
|
22
22
|
SendMessage,
|
|
23
|
+
SendOptions,
|
|
23
24
|
SessionDeviceStatus,
|
|
24
25
|
UpdateModelOptions,
|
|
25
26
|
UpdateModelResult,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
resolveDreamingSettings,
|
|
44
45
|
sameContextCandidates,
|
|
45
46
|
toBaseModelHandle,
|
|
47
|
+
turnSendOptions,
|
|
46
48
|
type NormalizedUpdateModelInput,
|
|
47
49
|
type RemoteClientSessionCoreConfig,
|
|
48
50
|
type RemoteClientRuntimeController,
|
|
@@ -78,6 +80,9 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
78
80
|
private initializePromise: Promise<SDKInitMessage> | null = null;
|
|
79
81
|
private removeMessageHandler: (() => void) | null = null;
|
|
80
82
|
private detachTransportDisconnect: (() => void) | null = null;
|
|
83
|
+
private idleTransportDisconnected = false;
|
|
84
|
+
private transportRecoveryPromise: Promise<void> | null = null;
|
|
85
|
+
private transportDisconnectGeneration = 0;
|
|
81
86
|
private readonly turns: RemoteTurnCoordinator;
|
|
82
87
|
private toolNames: string[] | undefined;
|
|
83
88
|
private deviceStatusListeners = new Set<(status: SessionDeviceStatus) => void>();
|
|
@@ -122,8 +127,7 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
122
127
|
const attempt = this.performInitialize();
|
|
123
128
|
const memo = attempt
|
|
124
129
|
.catch((error: unknown) => {
|
|
125
|
-
// Tear down only this attempt's partial state.
|
|
126
|
-
// session here: a failed remote attempt is retryable.
|
|
130
|
+
// Tear down only this attempt's partial state. A failed remote attempt is retryable.
|
|
127
131
|
this.cleanupFailedInitialize();
|
|
128
132
|
throw error;
|
|
129
133
|
})
|
|
@@ -164,8 +168,7 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
164
168
|
throw new Error("Session is closed");
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
//
|
|
168
|
-
// await initializePromise until every post-initialize option is applied.
|
|
171
|
+
// Lazy entry points must await this lifecycle commit after all post-initialize options.
|
|
169
172
|
this.initialized = true;
|
|
170
173
|
|
|
171
174
|
const initMessage: SDKInitMessage = {
|
|
@@ -206,21 +209,26 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
206
209
|
this.initialized = false;
|
|
207
210
|
}
|
|
208
211
|
|
|
209
|
-
async send(message: SendMessage): Promise<void> {
|
|
212
|
+
async send(message: SendMessage, options?: SendOptions): 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, options?.otid);
|
|
220
230
|
try {
|
|
221
|
-
|
|
222
|
-
clientMessageId: turn.clientMessageId,
|
|
223
|
-
});
|
|
231
|
+
controller.sendTurnMessage(runtime, message, turnSendOptions(turn));
|
|
224
232
|
} catch (error) {
|
|
225
233
|
this.turns.removeTrackedTurn(turn);
|
|
226
234
|
throw error;
|
|
@@ -317,6 +325,11 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
317
325
|
}
|
|
318
326
|
|
|
319
327
|
async updateModel(update: string | UpdateModelOptions): Promise<UpdateModelResult> {
|
|
328
|
+
if (this.mode.kind === "session" && this.mode.options.stateless === true) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
"updateModel() is unavailable in a stateless session because it changes persisted agent configuration.",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
320
333
|
if (!this.initialized) {
|
|
321
334
|
await this.initialize();
|
|
322
335
|
}
|
|
@@ -556,6 +569,7 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
556
569
|
this.deviceStatusListeners.clear();
|
|
557
570
|
this.controller?.close();
|
|
558
571
|
this.controller = null;
|
|
572
|
+
this.idleTransportDisconnected = false;
|
|
559
573
|
this.onCoreClose();
|
|
560
574
|
}
|
|
561
575
|
|
|
@@ -625,27 +639,31 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
625
639
|
protected abstract initializeRuntimeController(): Promise<RuntimeSessionInit>;
|
|
626
640
|
|
|
627
641
|
/**
|
|
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.
|
|
642
|
+
* Active turns fail because the input may have reached the listener. Cloud
|
|
643
|
+
* sessions may recover an idle connection before the next turn is tracked.
|
|
642
644
|
*/
|
|
643
|
-
protected watchTransportDisconnect(
|
|
644
|
-
onDisconnect(handler: () => void): () => void
|
|
645
|
-
|
|
645
|
+
protected watchTransportDisconnect(
|
|
646
|
+
client: { onDisconnect(handler: () => void): () => void },
|
|
647
|
+
options: { recoverWhenIdle?: boolean } = {},
|
|
648
|
+
): void {
|
|
646
649
|
this.detachTransportDisconnect?.();
|
|
647
650
|
this.detachTransportDisconnect = client.onDisconnect(() => {
|
|
648
651
|
if (this.closed) return;
|
|
652
|
+
this.transportDisconnectGeneration += 1;
|
|
653
|
+
if (options.recoverWhenIdle && !this.turns.hasInFlightTurn()) {
|
|
654
|
+
this.detachTransportDisconnect?.();
|
|
655
|
+
this.detachTransportDisconnect = null;
|
|
656
|
+
this.removeMessageHandler?.();
|
|
657
|
+
this.removeMessageHandler = null;
|
|
658
|
+
this.controller?.close();
|
|
659
|
+
this.controller = null;
|
|
660
|
+
const error = new Error(`${this.label} connection closed before send`);
|
|
661
|
+
for (const cancel of [...this.deviceStatusRefreshCancels]) cancel(error);
|
|
662
|
+
this.deviceStatusRefreshCancels.clear();
|
|
663
|
+
this.onIdleTransportDisconnect();
|
|
664
|
+
this.idleTransportDisconnected = true;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
649
667
|
this.turns.closeWithError(
|
|
650
668
|
`The ${this.label} connection closed unexpectedly; resume the conversation to continue.`,
|
|
651
669
|
);
|
|
@@ -653,6 +671,62 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
653
671
|
});
|
|
654
672
|
}
|
|
655
673
|
|
|
674
|
+
private async recoverIdleTransportIfNeeded(): Promise<void> {
|
|
675
|
+
if (!this.idleTransportDisconnected) return;
|
|
676
|
+
if (this.transportRecoveryPromise) {
|
|
677
|
+
await this.transportRecoveryPromise;
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const runtime = this.runtime;
|
|
681
|
+
if (!runtime) throw new Error("Session transport disconnected without a runtime");
|
|
682
|
+
const generation = this.transportDisconnectGeneration;
|
|
683
|
+
const recovery = this.recoverIdleTransport(runtime)
|
|
684
|
+
.then(async (init) => {
|
|
685
|
+
if (this.closed || generation !== this.transportDisconnectGeneration) {
|
|
686
|
+
init.controller.close();
|
|
687
|
+
this.onRecoveredTransportDiscarded();
|
|
688
|
+
throw new Error(`${this.label} connection closed during recovery`);
|
|
689
|
+
}
|
|
690
|
+
if (
|
|
691
|
+
init.runtime.agent_id !== runtime.agent_id ||
|
|
692
|
+
init.runtime.conversation_id !== runtime.conversation_id
|
|
693
|
+
) {
|
|
694
|
+
init.controller.close();
|
|
695
|
+
this.onRecoveredTransportDiscarded();
|
|
696
|
+
throw new Error(`${this.label} transport recovered a different runtime`);
|
|
697
|
+
}
|
|
698
|
+
this.controller = init.controller;
|
|
699
|
+
this.runtime = init.runtime;
|
|
700
|
+
this._modelSettings = init.modelSettings ?? this._modelSettings;
|
|
701
|
+
if (typeof init.model === "string" && init.model) this._model = init.model;
|
|
702
|
+
if (init.tools !== undefined) this.toolNames = init.tools;
|
|
703
|
+
this.removeMessageHandler = this.controller.onMessage((message) => {
|
|
704
|
+
if (this.runtime) this.turns.handleProtocolMessage(message, this.runtime);
|
|
705
|
+
});
|
|
706
|
+
this.idleTransportDisconnected = false;
|
|
707
|
+
await this.afterRuntimeInitialized();
|
|
708
|
+
})
|
|
709
|
+
.finally(() => {
|
|
710
|
+
if (this.transportRecoveryPromise === recovery) this.transportRecoveryPromise = null;
|
|
711
|
+
});
|
|
712
|
+
this.transportRecoveryPromise = recovery;
|
|
713
|
+
await recovery;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
protected async recoverIdleTransport(
|
|
717
|
+
_runtime: RuntimeScope,
|
|
718
|
+
): Promise<RuntimeSessionInit> {
|
|
719
|
+
throw new Error(`${this.label} sessions do not support idle transport recovery`);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
protected onIdleTransportDisconnect(): void {
|
|
723
|
+
// Optional hook for transport-specific handler cleanup before recovery.
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
protected onRecoveredTransportDiscarded(): void {
|
|
727
|
+
// Optional hook when a recovery finishes after the session closed or dropped again.
|
|
728
|
+
}
|
|
729
|
+
|
|
656
730
|
protected async afterRuntimeInitialized(): Promise<void> {
|
|
657
731
|
// Optional hook for subclasses to send transport-specific startup frames.
|
|
658
732
|
}
|
|
@@ -779,7 +853,9 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
779
853
|
}
|
|
780
854
|
|
|
781
855
|
const dreamingSettings = resolveDreamingSettings(options.dreaming);
|
|
782
|
-
|
|
856
|
+
const isStatelessSession =
|
|
857
|
+
this.mode.kind === "session" && this.mode.options.stateless === true;
|
|
858
|
+
if (dreamingSettings && !isStatelessSession) {
|
|
783
859
|
const response = await this.controller.request(
|
|
784
860
|
"set_reflection_settings",
|
|
785
861
|
{
|
|
@@ -55,6 +55,8 @@ export type RuntimeTurnResult = {
|
|
|
55
55
|
|
|
56
56
|
export type RuntimeSendTurnOptions = {
|
|
57
57
|
clientMessageId: string;
|
|
58
|
+
/** Caller-supplied OTID for the user message, when one was provided. */
|
|
59
|
+
otid?: string;
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
export type RuntimeRequestOptions = {
|
|
@@ -128,6 +130,8 @@ export type TurnTracker = {
|
|
|
128
130
|
id: number;
|
|
129
131
|
runtime: RuntimeScope;
|
|
130
132
|
clientMessageId: string;
|
|
133
|
+
/** Caller-supplied OTID for this turn's user message, when one was provided. */
|
|
134
|
+
otid?: string;
|
|
131
135
|
queuedAt: number;
|
|
132
136
|
startedAt: number;
|
|
133
137
|
assistantText: string;
|
|
@@ -677,3 +681,23 @@ export function toSessionDeviceStatus(
|
|
|
677
681
|
export function normalizeSendMessage(message: SendMessage): string | MessageContentItem[] {
|
|
678
682
|
return message;
|
|
679
683
|
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Validate a caller-supplied OTID. Empty/whitespace-only values would silently
|
|
687
|
+
* defeat correlation, so they are rejected instead of being dropped.
|
|
688
|
+
*/
|
|
689
|
+
export function normalizeCallerOtid(otid: string | undefined): string | undefined {
|
|
690
|
+
if (otid === undefined) return undefined;
|
|
691
|
+
if (typeof otid !== "string" || otid.trim() === "") {
|
|
692
|
+
throw new Error("send() otid must be a non-empty string");
|
|
693
|
+
}
|
|
694
|
+
return otid;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Wire correlation options for a tracked turn. */
|
|
698
|
+
export function turnSendOptions(turn: TurnTracker): RuntimeSendTurnOptions {
|
|
699
|
+
return {
|
|
700
|
+
clientMessageId: turn.clientMessageId,
|
|
701
|
+
...(turn.otid !== undefined ? { otid: turn.otid } : {}),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isApprovalConflictSignal,
|
|
16
16
|
loopStatusRunIds,
|
|
17
17
|
loopStatusValue,
|
|
18
|
+
normalizeCallerOtid,
|
|
18
19
|
queueItems,
|
|
19
20
|
sameRuntime,
|
|
20
21
|
streamDeltaMessageType,
|
|
@@ -76,11 +77,19 @@ export class RemoteTurnCoordinator {
|
|
|
76
77
|
return this.activeTurn !== null || this.pendingTurns.length > 0;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Start tracking a turn. A caller-supplied OTID doubles as the turn's
|
|
82
|
+
* `clientMessageId` so queue updates carry the same correlation id the
|
|
83
|
+
* persisted message will; otherwise the SDK mints one.
|
|
84
|
+
*/
|
|
85
|
+
trackSentTurn(runtime: RuntimeScope, callerOtid?: string): TurnTracker {
|
|
86
|
+
const otid = normalizeCallerOtid(callerOtid);
|
|
80
87
|
const turn: TurnTracker = {
|
|
81
88
|
id: ++this.nextTurnId,
|
|
82
89
|
runtime,
|
|
83
|
-
|
|
90
|
+
...(otid !== undefined ? { otid } : {}),
|
|
91
|
+
clientMessageId:
|
|
92
|
+
otid ?? `sdk-message-${Date.now()}-${++this.clientMessageCounter}`,
|
|
84
93
|
queuedAt: Date.now(),
|
|
85
94
|
startedAt: 0,
|
|
86
95
|
assistantText: "",
|
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
|
}
|