@letta-ai/letta-agent-sdk 0.3.2 → 0.5.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 +44 -0
- package/README.md +29 -0
- 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 +979 -1010
- package/dist/client-entry.js.map +15 -11
- package/dist/client.d.ts +3 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/cloud-sandbox.d.ts +33 -0
- package/dist/cloud-sandbox.d.ts.map +1 -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 +3242 -4298
- package/dist/index.js.map +20 -17
- 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-client-session-core.d.ts +6 -93
- package/dist/remote-client-session-core.d.ts.map +1 -1
- package/dist/remote-session-protocol.d.ts +132 -0
- package/dist/remote-session-protocol.d.ts.map +1 -0
- package/dist/remote-turn-coordinator.d.ts +49 -0
- package/dist/remote-turn-coordinator.d.ts.map +1 -0
- package/dist/types.d.ts +59 -86
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/app-server-management.ts +405 -0
- package/src/app-server-session.ts +916 -0
- package/src/cli-resolver.ts +46 -0
- package/src/client-base.ts +456 -0
- package/src/client-entry.ts +31 -0
- package/src/client.ts +99 -0
- package/src/cloud-management.ts +360 -0
- package/src/cloud-sandbox.ts +117 -0
- package/src/cloud-session.ts +1055 -0
- package/src/cloud-status-transport.ts +305 -0
- package/src/index.ts +422 -0
- package/src/interactiveToolPolicy.ts +62 -0
- package/src/local-app-server-session.ts +49 -0
- package/src/local-app-server.ts +231 -0
- package/src/management-types.ts +133 -0
- package/src/management.ts +199 -0
- package/src/remote-client-session-core.ts +786 -0
- package/src/remote-session-protocol.ts +674 -0
- package/src/remote-turn-coordinator.ts +523 -0
- package/src/remote.ts +177 -0
- package/src/repositories.ts +340 -0
- package/src/request-ids.ts +33 -0
- package/src/stream-events.ts +88 -0
- package/src/tool-helpers.ts +147 -0
- package/src/types.ts +1281 -0
- package/src/validation.ts +238 -0
- package/src/websocket.ts +22 -0
- 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
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppServerSocketConstructor,
|
|
3
|
+
AppServerSocketLike,
|
|
4
|
+
AppServerSocketOptions,
|
|
5
|
+
} from "@letta-ai/letta-code/app-server-client";
|
|
6
|
+
import type { ProtocolMessage, RuntimeScope } from "./remote-session-protocol.js";
|
|
7
|
+
import type {
|
|
8
|
+
LettaCodeSocketConstructor,
|
|
9
|
+
LettaCodeSocketLike,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
type CloudStatusMessage = ProtocolMessage & {
|
|
13
|
+
type: string;
|
|
14
|
+
seq?: unknown;
|
|
15
|
+
event_seq?: unknown;
|
|
16
|
+
idempotency_key?: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type CloudStatusTransportOptions = {
|
|
20
|
+
url: string;
|
|
21
|
+
WebSocket: LettaCodeSocketConstructor;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
pingIntervalMs: number;
|
|
24
|
+
runtime: RuntimeScope;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type SocketChannel = "control" | "stream";
|
|
28
|
+
type Listener = (event: unknown) => void;
|
|
29
|
+
|
|
30
|
+
const CONNECTING = 0;
|
|
31
|
+
const OPEN = 1;
|
|
32
|
+
const CLOSING = 2;
|
|
33
|
+
const CLOSED = 3;
|
|
34
|
+
const MAX_IDEMPOTENCY_KEYS = 1_000;
|
|
35
|
+
|
|
36
|
+
function addSocketListener(
|
|
37
|
+
socket: LettaCodeSocketLike,
|
|
38
|
+
type: string,
|
|
39
|
+
listener: Listener,
|
|
40
|
+
): () => void {
|
|
41
|
+
if (socket.addEventListener) {
|
|
42
|
+
socket.addEventListener(type, listener);
|
|
43
|
+
return () => socket.removeEventListener?.(type, listener);
|
|
44
|
+
}
|
|
45
|
+
if (socket.on) {
|
|
46
|
+
socket.on(type, listener);
|
|
47
|
+
return () => socket.off?.(type, listener);
|
|
48
|
+
}
|
|
49
|
+
throw new Error("WebSocket implementation does not support event listeners.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function messageEventData(event: unknown): string | null {
|
|
53
|
+
if (typeof event === "string") return event;
|
|
54
|
+
if (!event || typeof event !== "object") return null;
|
|
55
|
+
const data = (event as { data?: unknown }).data;
|
|
56
|
+
if (typeof data === "string") return data;
|
|
57
|
+
if (data instanceof ArrayBuffer) {
|
|
58
|
+
return new TextDecoder().decode(data);
|
|
59
|
+
}
|
|
60
|
+
if (data instanceof Uint8Array) {
|
|
61
|
+
return new TextDecoder().decode(data);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function channelUrl(url: string, channel: SocketChannel): string {
|
|
67
|
+
const parsed = new URL(url);
|
|
68
|
+
parsed.searchParams.set("channel", channel);
|
|
69
|
+
return parsed.toString();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Cloud's status relay still exposes separate control and stream sockets.
|
|
74
|
+
*
|
|
75
|
+
* This adapter presents those two relay sockets as one AppServerSocketLike,
|
|
76
|
+
* allowing CloudSession to reuse AppServerClient's request correlation and
|
|
77
|
+
* the SDK's shared runtime controller without weakening the local app-server's
|
|
78
|
+
* strict one-socket contract. Remove it when LET-10236 migrates the relay.
|
|
79
|
+
*/
|
|
80
|
+
export class CloudStatusTransport implements AppServerSocketLike {
|
|
81
|
+
readonly controlSocket: LettaCodeSocketLike;
|
|
82
|
+
readonly streamSocket: LettaCodeSocketLike;
|
|
83
|
+
|
|
84
|
+
private state = CONNECTING;
|
|
85
|
+
private openedChannels = new Set<SocketChannel>();
|
|
86
|
+
private listeners = new Map<string, Set<Listener>>();
|
|
87
|
+
private removers: Array<() => void> = [];
|
|
88
|
+
private seenIdempotencyKeys = new Set<string>();
|
|
89
|
+
private seenIdempotencyOrder: string[] = [];
|
|
90
|
+
private lastEventSeq: number | null = null;
|
|
91
|
+
private pingTimer: ReturnType<typeof setInterval> | null = null;
|
|
92
|
+
private closeEmitted = false;
|
|
93
|
+
|
|
94
|
+
constructor(private readonly options: CloudStatusTransportOptions) {
|
|
95
|
+
const socketOptions = options.headers
|
|
96
|
+
? { headers: options.headers }
|
|
97
|
+
: undefined;
|
|
98
|
+
this.controlSocket = new options.WebSocket(
|
|
99
|
+
channelUrl(options.url, "control"),
|
|
100
|
+
socketOptions,
|
|
101
|
+
);
|
|
102
|
+
this.streamSocket = new options.WebSocket(
|
|
103
|
+
channelUrl(options.url, "stream"),
|
|
104
|
+
socketOptions,
|
|
105
|
+
);
|
|
106
|
+
this.bindSocket("control", this.controlSocket);
|
|
107
|
+
this.bindSocket("stream", this.streamSocket);
|
|
108
|
+
this.startPing();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get readyState(): number {
|
|
112
|
+
return this.state;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
send(data: string): void {
|
|
116
|
+
if (this.state !== OPEN || this.controlSocket.readyState !== OPEN) {
|
|
117
|
+
throw new Error("Cloud status control socket is not open");
|
|
118
|
+
}
|
|
119
|
+
this.controlSocket.send(data);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
close(): void {
|
|
123
|
+
if (this.state === CLOSED || this.state === CLOSING) return;
|
|
124
|
+
this.state = CLOSING;
|
|
125
|
+
this.stopPing();
|
|
126
|
+
this.closeUnderlyingSockets();
|
|
127
|
+
this.finishClose({});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
addEventListener(type: string, listener: Listener): void {
|
|
131
|
+
let listeners = this.listeners.get(type);
|
|
132
|
+
if (!listeners) {
|
|
133
|
+
listeners = new Set();
|
|
134
|
+
this.listeners.set(type, listeners);
|
|
135
|
+
}
|
|
136
|
+
listeners.add(listener);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
removeEventListener(type: string, listener: Listener): void {
|
|
140
|
+
this.listeners.get(type)?.delete(listener);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private bindSocket(
|
|
144
|
+
channel: SocketChannel,
|
|
145
|
+
socket: LettaCodeSocketLike,
|
|
146
|
+
): void {
|
|
147
|
+
this.removers.push(
|
|
148
|
+
addSocketListener(socket, "open", () => {
|
|
149
|
+
this.openedChannels.add(channel);
|
|
150
|
+
if (this.openedChannels.size === 2 && this.state === CONNECTING) {
|
|
151
|
+
this.state = OPEN;
|
|
152
|
+
this.emit("open", {});
|
|
153
|
+
}
|
|
154
|
+
}),
|
|
155
|
+
addSocketListener(socket, "message", (event) => {
|
|
156
|
+
if (this.shouldForwardMessage(channel, socket, event)) {
|
|
157
|
+
this.emit("message", event);
|
|
158
|
+
}
|
|
159
|
+
}),
|
|
160
|
+
addSocketListener(socket, "error", (event) => {
|
|
161
|
+
this.emit("error", event);
|
|
162
|
+
}),
|
|
163
|
+
addSocketListener(socket, "close", (event) => {
|
|
164
|
+
if (this.state !== CLOSED) {
|
|
165
|
+
this.state = CLOSING;
|
|
166
|
+
this.stopPing();
|
|
167
|
+
this.closeUnderlyingSockets(socket);
|
|
168
|
+
this.finishClose(event);
|
|
169
|
+
}
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private shouldForwardMessage(
|
|
175
|
+
channel: SocketChannel,
|
|
176
|
+
socket: LettaCodeSocketLike,
|
|
177
|
+
event: unknown,
|
|
178
|
+
): boolean {
|
|
179
|
+
const data = messageEventData(event);
|
|
180
|
+
if (!data) return true;
|
|
181
|
+
let message: CloudStatusMessage;
|
|
182
|
+
try {
|
|
183
|
+
message = JSON.parse(data) as CloudStatusMessage;
|
|
184
|
+
} catch {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.ackIfSequenced(socket, message);
|
|
189
|
+
if (channel === "control" && message.type === "stream_delta") {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (this.isDuplicate(message)) return false;
|
|
193
|
+
this.trackEventSequence(message);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private ackIfSequenced(
|
|
198
|
+
socket: LettaCodeSocketLike,
|
|
199
|
+
message: CloudStatusMessage,
|
|
200
|
+
): void {
|
|
201
|
+
if (typeof message.seq !== "number") return;
|
|
202
|
+
this.sendCommand(socket, { type: "ack", seq: message.seq });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private isDuplicate(message: CloudStatusMessage): boolean {
|
|
206
|
+
const key =
|
|
207
|
+
typeof message.idempotency_key === "string"
|
|
208
|
+
? message.idempotency_key
|
|
209
|
+
: null;
|
|
210
|
+
if (!key) return false;
|
|
211
|
+
if (this.seenIdempotencyKeys.has(key)) return true;
|
|
212
|
+
this.seenIdempotencyKeys.add(key);
|
|
213
|
+
this.seenIdempotencyOrder.push(key);
|
|
214
|
+
while (this.seenIdempotencyOrder.length > MAX_IDEMPOTENCY_KEYS) {
|
|
215
|
+
const oldest = this.seenIdempotencyOrder.shift();
|
|
216
|
+
if (oldest) this.seenIdempotencyKeys.delete(oldest);
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private trackEventSequence(message: CloudStatusMessage): void {
|
|
222
|
+
if (typeof message.event_seq !== "number") return;
|
|
223
|
+
if (
|
|
224
|
+
this.lastEventSeq !== null &&
|
|
225
|
+
message.event_seq > this.lastEventSeq + 1
|
|
226
|
+
) {
|
|
227
|
+
this.sendCommand(this.controlSocket, {
|
|
228
|
+
type: "sync",
|
|
229
|
+
runtime: this.options.runtime,
|
|
230
|
+
recover_approvals: true,
|
|
231
|
+
force_device_status: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
this.lastEventSeq === null ||
|
|
236
|
+
message.event_seq > this.lastEventSeq
|
|
237
|
+
) {
|
|
238
|
+
this.lastEventSeq = message.event_seq;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private sendCommand(
|
|
243
|
+
socket: LettaCodeSocketLike,
|
|
244
|
+
command: Record<string, unknown>,
|
|
245
|
+
): void {
|
|
246
|
+
if (socket.readyState !== OPEN) return;
|
|
247
|
+
try {
|
|
248
|
+
socket.send(JSON.stringify(command));
|
|
249
|
+
} catch {
|
|
250
|
+
// ACK, sync recovery, and ping are best-effort relay reliability frames.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private startPing(): void {
|
|
255
|
+
this.pingTimer = setInterval(() => {
|
|
256
|
+
this.sendCommand(this.controlSocket, { type: "ping" });
|
|
257
|
+
this.sendCommand(this.streamSocket, { type: "ping" });
|
|
258
|
+
}, this.options.pingIntervalMs);
|
|
259
|
+
(this.pingTimer as { unref?: () => void }).unref?.();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private stopPing(): void {
|
|
263
|
+
if (!this.pingTimer) return;
|
|
264
|
+
clearInterval(this.pingTimer);
|
|
265
|
+
this.pingTimer = null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private closeUnderlyingSockets(
|
|
269
|
+
except?: LettaCodeSocketLike,
|
|
270
|
+
): void {
|
|
271
|
+
for (const socket of [this.controlSocket, this.streamSocket]) {
|
|
272
|
+
if (
|
|
273
|
+
socket !== except &&
|
|
274
|
+
(socket.readyState === CONNECTING || socket.readyState === OPEN)
|
|
275
|
+
) {
|
|
276
|
+
socket.close();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private finishClose(event: unknown): void {
|
|
282
|
+
if (this.closeEmitted) return;
|
|
283
|
+
this.closeEmitted = true;
|
|
284
|
+
this.state = CLOSED;
|
|
285
|
+
this.stopPing();
|
|
286
|
+
for (const remove of this.removers.splice(0)) remove();
|
|
287
|
+
this.emit("close", event);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private emit(type: string, event: unknown): void {
|
|
291
|
+
for (const listener of this.listeners.get(type) ?? []) {
|
|
292
|
+
listener(event);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function createCloudStatusTransportConstructor(
|
|
298
|
+
options: CloudStatusTransportOptions,
|
|
299
|
+
): AppServerSocketConstructor {
|
|
300
|
+
return class CloudStatusTransportSocket extends CloudStatusTransport {
|
|
301
|
+
constructor(_url: string, _socketOptions?: AppServerSocketOptions) {
|
|
302
|
+
super(options);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|