@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
|
@@ -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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
* const agentId = await client.createAgent();
|
|
12
12
|
* const clientSession = client.resumeSession(agentId);
|
|
13
13
|
*
|
|
14
|
-
* // Start session with default agent + new conversation (like `letta`)
|
|
15
|
-
* const session = createSession();
|
|
16
|
-
*
|
|
17
14
|
* // Create a new agent explicitly
|
|
18
15
|
* const agentId = await createAgent();
|
|
19
16
|
*
|
|
@@ -26,13 +23,11 @@
|
|
|
26
23
|
* // Create new conversation on specific agent
|
|
27
24
|
* const session = createSession(agentId);
|
|
28
25
|
*
|
|
29
|
-
* // One-shot prompt
|
|
30
|
-
* const result = await prompt('Hello');
|
|
31
|
-
* const result = await prompt('Hello', agentId); // specific agent
|
|
26
|
+
* // One-shot prompt in a new conversation
|
|
27
|
+
* const result = await prompt('Hello', agentId);
|
|
32
28
|
* ```
|
|
33
29
|
*/
|
|
34
30
|
|
|
35
|
-
import { Session } from "./session.js";
|
|
36
31
|
import { LettaAgentClient } from "./client.js";
|
|
37
32
|
import type {
|
|
38
33
|
CreateSessionOptions,
|
|
@@ -98,6 +93,7 @@ export type {
|
|
|
98
93
|
SessionDiffPreview,
|
|
99
94
|
SkillSource,
|
|
100
95
|
DreamingOptions,
|
|
96
|
+
SessionDreamingOptions,
|
|
101
97
|
DreamingTrigger,
|
|
102
98
|
DreamingBehavior,
|
|
103
99
|
EffectiveDreamingSettings,
|
|
@@ -164,7 +160,6 @@ export type {
|
|
|
164
160
|
ConversationMessagesResult,
|
|
165
161
|
} from "./management-types.js";
|
|
166
162
|
|
|
167
|
-
export { Session } from "./session.js";
|
|
168
163
|
export { RepositoriesClient } from "./repositories.js";
|
|
169
164
|
export { LettaAgentClient } from "./client.js";
|
|
170
165
|
export { CloudManagedSandboxExpiredError } from "./cloud-session.js";
|
|
@@ -210,29 +205,20 @@ export async function createAgent(options: CreateAgentOptions = {}): Promise<str
|
|
|
210
205
|
/**
|
|
211
206
|
* Create a new conversation (session).
|
|
212
207
|
*
|
|
213
|
-
*
|
|
214
|
-
* - With agentId: creates new conversation on specified agent
|
|
208
|
+
* Creates a new conversation on the specified agent.
|
|
215
209
|
*
|
|
216
210
|
* @example
|
|
217
211
|
* ```typescript
|
|
218
|
-
* // New conversation on default agent (like `letta`)
|
|
219
|
-
* await using session = createSession();
|
|
220
|
-
*
|
|
221
212
|
* // New conversation on specific agent
|
|
222
213
|
* await using session = createSession(agentId);
|
|
223
214
|
* ```
|
|
224
215
|
*/
|
|
225
216
|
export function createSession(
|
|
226
|
-
agentId
|
|
217
|
+
agentId: string,
|
|
227
218
|
options: CreateSessionOptions = {},
|
|
228
219
|
): LettaCodeSession {
|
|
229
220
|
validateCreateSessionOptions(options);
|
|
230
|
-
|
|
231
|
-
return new LettaAgentClient().createSession(agentId, options);
|
|
232
|
-
}
|
|
233
|
-
// The app-server runtime_start protocol requires an explicit agent id. Keep
|
|
234
|
-
// the historical default/LRU-agent helper on the legacy stdio transport.
|
|
235
|
-
return new LettaAgentClient({ backend: "local", transport: "stdio" }).createSession(options);
|
|
221
|
+
return new LettaAgentClient().createSession(agentId, options);
|
|
236
222
|
}
|
|
237
223
|
|
|
238
224
|
/**
|
|
@@ -264,13 +250,11 @@ export function resumeSession(
|
|
|
264
250
|
/**
|
|
265
251
|
* One-shot prompt convenience function.
|
|
266
252
|
*
|
|
267
|
-
*
|
|
268
|
-
* - With agentId: uses specific agent, new conversation
|
|
253
|
+
* Uses the specified agent in a new conversation.
|
|
269
254
|
* - Uses a short-lived session and returns the final turn result.
|
|
270
255
|
*
|
|
271
256
|
* @example
|
|
272
257
|
* ```typescript
|
|
273
|
-
* const result = await prompt('What is 2+2?'); // default agent
|
|
274
258
|
* const result = await prompt('What is the capital of France?', agentId); // specific agent
|
|
275
259
|
* ```
|
|
276
260
|
*/
|
|
@@ -283,13 +267,11 @@ type InitializableSession = LettaCodeSession & {
|
|
|
283
267
|
};
|
|
284
268
|
|
|
285
269
|
export async function prompt(
|
|
286
|
-
message:
|
|
287
|
-
agentId
|
|
270
|
+
message: SendMessage,
|
|
271
|
+
agentId: string,
|
|
272
|
+
options: CreateSessionOptions = {},
|
|
288
273
|
): Promise<SDKResultMessage> {
|
|
289
|
-
|
|
290
|
-
const session = agentId
|
|
291
|
-
? createSession(agentId)
|
|
292
|
-
: createSession();
|
|
274
|
+
const session = createSession(agentId, options);
|
|
293
275
|
|
|
294
276
|
try {
|
|
295
277
|
return await (session as TurnSession).runTurn(message);
|
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
type AppServerSessionOptions,
|
|
5
5
|
} from "./app-server-session.js";
|
|
6
6
|
import { startLocalAppServer } from "./local-app-server.js";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
LettaCodeClientSessionOptions,
|
|
9
|
+
LettaCodeLocalAppServerOptions,
|
|
10
|
+
} from "./types.js";
|
|
8
11
|
|
|
9
12
|
export function createLocalAppServerSession(
|
|
10
13
|
options: LettaCodeLocalAppServerOptions | undefined,
|
|
11
14
|
mode: AppServerSessionMode,
|
|
12
|
-
beforeConnect?: () => Promise<void>,
|
|
13
15
|
): AppServerSession {
|
|
14
16
|
const appServer = options ?? {};
|
|
15
17
|
const sessionOptions: AppServerSessionOptions = {
|
|
@@ -22,6 +24,15 @@ export function createLocalAppServerSession(
|
|
|
22
24
|
backend: appServer.harnessBackend ?? "local",
|
|
23
25
|
startupTimeoutMs: appServer.startupTimeoutMs,
|
|
24
26
|
env: sessionEnv,
|
|
27
|
+
filesystemConfinement:
|
|
28
|
+
mode.kind === "session"
|
|
29
|
+
? (mode.options as LettaCodeClientSessionOptions)
|
|
30
|
+
.filesystemConfinement
|
|
31
|
+
: undefined,
|
|
32
|
+
agentId:
|
|
33
|
+
mode.kind === "session" && "agentId" in mode
|
|
34
|
+
? mode.agentId
|
|
35
|
+
: undefined,
|
|
25
36
|
}),
|
|
26
37
|
}),
|
|
27
38
|
...(appServer.WebSocket !== undefined
|
|
@@ -33,7 +44,6 @@ export function createLocalAppServerSession(
|
|
|
33
44
|
...(appServer.pinGlobalAgent !== undefined
|
|
34
45
|
? { pinGlobalAgent: appServer.pinGlobalAgent }
|
|
35
46
|
: {}),
|
|
36
|
-
...(beforeConnect ? { beforeConnect } : {}),
|
|
37
47
|
};
|
|
38
48
|
return new AppServerSession(sessionOptions, mode);
|
|
39
49
|
}
|
package/src/local-app-server.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
createMemoryConfinementLauncher,
|
|
6
|
+
type MemoryConfinementLauncherInput,
|
|
7
|
+
type MemoryConfinementLauncherResult,
|
|
8
|
+
} from "@letta-ai/letta-code/memory-confinement";
|
|
2
9
|
import { findLettaCli } from "./cli-resolver.js";
|
|
3
10
|
|
|
4
11
|
export interface LocalAppServerHandle {
|
|
@@ -12,12 +19,70 @@ export interface StartLocalAppServerOptions {
|
|
|
12
19
|
startupTimeoutMs?: number;
|
|
13
20
|
cliPath?: string;
|
|
14
21
|
env?: Record<string, string | undefined>;
|
|
22
|
+
filesystemConfinement?: "memory";
|
|
23
|
+
agentId?: string;
|
|
15
24
|
}
|
|
16
25
|
|
|
26
|
+
interface LocalAppServerProcess {
|
|
27
|
+
command: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
env: NodeJS.ProcessEnv;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type MemoryConfinementLauncher = (
|
|
33
|
+
input: MemoryConfinementLauncherInput,
|
|
34
|
+
) => MemoryConfinementLauncherResult;
|
|
35
|
+
|
|
17
36
|
const DEFAULT_LISTEN_URL = "ws://127.0.0.1:0";
|
|
18
37
|
const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
19
38
|
const LISTENING_RE = /^Listening on\s+(ws:\/\/\S+)\s*$/m;
|
|
20
39
|
|
|
40
|
+
function targetHomeDirectory(env: NodeJS.ProcessEnv): string {
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
const profile = env.USERPROFILE?.trim();
|
|
43
|
+
if (profile) return profile;
|
|
44
|
+
const drive = env.HOMEDRIVE?.trim();
|
|
45
|
+
const path = env.HOMEPATH?.trim();
|
|
46
|
+
if (drive && path) return `${drive}${path}`;
|
|
47
|
+
return homedir();
|
|
48
|
+
}
|
|
49
|
+
return env.HOME?.trim() || homedir();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function withDefaultMemoryDirectory(
|
|
53
|
+
env: NodeJS.ProcessEnv,
|
|
54
|
+
options: StartLocalAppServerOptions,
|
|
55
|
+
): NodeJS.ProcessEnv {
|
|
56
|
+
const explicitMemoryDir = options.env?.MEMORY_DIR?.trim();
|
|
57
|
+
const explicitLettaMemoryDir = options.env?.LETTA_MEMORY_DIR?.trim();
|
|
58
|
+
const scopedEnv = { ...env };
|
|
59
|
+
if (explicitMemoryDir || explicitLettaMemoryDir) {
|
|
60
|
+
if (!explicitMemoryDir) delete scopedEnv.MEMORY_DIR;
|
|
61
|
+
if (!explicitLettaMemoryDir) delete scopedEnv.LETTA_MEMORY_DIR;
|
|
62
|
+
return scopedEnv;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Never inherit the SDK process's agent scope into a different session.
|
|
66
|
+
delete scopedEnv.MEMORY_DIR;
|
|
67
|
+
delete scopedEnv.LETTA_MEMORY_DIR;
|
|
68
|
+
if (!options.agentId) return scopedEnv;
|
|
69
|
+
|
|
70
|
+
const homeDir = targetHomeDirectory(env);
|
|
71
|
+
// Mirrors Letta Code's getScopedMemoryFilesystemRoot contract. The SDK pins
|
|
72
|
+
// an exact Letta Code version so the launch policy and layout move together.
|
|
73
|
+
const memoryDir =
|
|
74
|
+
options.backend === "api"
|
|
75
|
+
? join(homeDir, ".letta", "agents", options.agentId, "memory")
|
|
76
|
+
: join(
|
|
77
|
+
env.LETTA_LOCAL_BACKEND_DIR?.trim() ||
|
|
78
|
+
join(homeDir, ".letta", "lc-local-backend"),
|
|
79
|
+
"memfs",
|
|
80
|
+
options.agentId,
|
|
81
|
+
"memory",
|
|
82
|
+
);
|
|
83
|
+
return { ...scopedEnv, MEMORY_DIR: memoryDir };
|
|
84
|
+
}
|
|
85
|
+
|
|
21
86
|
function appendLine(buffer: string, chunk: unknown): string {
|
|
22
87
|
return buffer + String(chunk);
|
|
23
88
|
}
|
|
@@ -40,6 +105,35 @@ export function buildLocalAppServerArgs(
|
|
|
40
105
|
];
|
|
41
106
|
}
|
|
42
107
|
|
|
108
|
+
export function buildLocalAppServerProcess(
|
|
109
|
+
cliPath: string,
|
|
110
|
+
options: StartLocalAppServerOptions = {},
|
|
111
|
+
confineMemory: MemoryConfinementLauncher = createMemoryConfinementLauncher,
|
|
112
|
+
): LocalAppServerProcess {
|
|
113
|
+
const env = { ...process.env, ...(options.env ?? {}) };
|
|
114
|
+
const launcher = [
|
|
115
|
+
process.execPath,
|
|
116
|
+
...buildLocalAppServerArgs(cliPath, options),
|
|
117
|
+
];
|
|
118
|
+
if (options.filesystemConfinement !== "memory") {
|
|
119
|
+
return {
|
|
120
|
+
command: launcher[0] as string,
|
|
121
|
+
args: launcher.slice(1),
|
|
122
|
+
env,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const confined = confineMemory({
|
|
127
|
+
launcher,
|
|
128
|
+
env: withDefaultMemoryDirectory(env, options),
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
command: confined.launcher[0] as string,
|
|
132
|
+
args: confined.launcher.slice(1),
|
|
133
|
+
env: confined.env,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
43
137
|
function terminateProcess(child: ChildProcess): void {
|
|
44
138
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
45
139
|
child.kill("SIGTERM");
|
|
@@ -57,13 +151,13 @@ export function startLocalAppServer(
|
|
|
57
151
|
options: StartLocalAppServerOptions = {},
|
|
58
152
|
): Promise<LocalAppServerHandle> {
|
|
59
153
|
const cliPath = options.cliPath ?? findLettaCli();
|
|
60
|
-
const
|
|
154
|
+
const processSpec = buildLocalAppServerProcess(cliPath, options);
|
|
61
155
|
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
62
156
|
|
|
63
157
|
return new Promise((resolve, reject) => {
|
|
64
|
-
const child = spawn(
|
|
158
|
+
const child = spawn(processSpec.command, processSpec.args, {
|
|
65
159
|
stdio: ["ignore", "pipe", "pipe"],
|
|
66
|
-
env:
|
|
160
|
+
env: processSpec.env,
|
|
67
161
|
});
|
|
68
162
|
|
|
69
163
|
let settled = false;
|
package/src/management.ts
CHANGED
|
@@ -45,13 +45,6 @@ export interface ManagementTransport {
|
|
|
45
45
|
conversationId: string,
|
|
46
46
|
query: ManagementQuery,
|
|
47
47
|
): Promise<ConversationMessagesResult>;
|
|
48
|
-
/**
|
|
49
|
-
* Release the pooled connection, waiting for in-flight work when necessary,
|
|
50
|
-
* so other clients of the same backend (for example sessions, which compete
|
|
51
|
-
* for the app-server's single control-client slot) can connect. Optional;
|
|
52
|
-
* transports whose backend has no such contention (Cloud REST) omit it.
|
|
53
|
-
*/
|
|
54
|
-
releaseIdleConnection?(): Promise<void> | void;
|
|
55
48
|
}
|
|
56
49
|
|
|
57
50
|
type TransportProvider = () => ManagementTransport;
|
|
@@ -450,6 +450,20 @@ export function streamDeltaRunId(delta: Record<string, unknown>): string | undef
|
|
|
450
450
|
return typeof delta.run_id === "string" ? delta.run_id : undefined;
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
export function streamDeltaOtid(
|
|
454
|
+
delta: Record<string, unknown>,
|
|
455
|
+
): string | null | undefined {
|
|
456
|
+
return typeof delta.otid === "string" || delta.otid === null
|
|
457
|
+
? delta.otid
|
|
458
|
+
: undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function streamDeltaSeqId(
|
|
462
|
+
delta: Record<string, unknown>,
|
|
463
|
+
): number | undefined {
|
|
464
|
+
return typeof delta.seq_id === "number" ? delta.seq_id : undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
453
467
|
export function streamDeltaStopReason(delta: Record<string, unknown>): string | null | undefined {
|
|
454
468
|
return typeof delta.stop_reason === "string" ? delta.stop_reason : undefined;
|
|
455
469
|
}
|
|
@@ -650,6 +664,10 @@ export function toSessionDeviceStatus(
|
|
|
650
664
|
isProcessing: status.is_processing,
|
|
651
665
|
permissionMode,
|
|
652
666
|
workingDirectory: status.current_working_directory,
|
|
667
|
+
memoryDirectory:
|
|
668
|
+
typeof status.memory_directory === "string"
|
|
669
|
+
? status.memory_directory
|
|
670
|
+
: null,
|
|
653
671
|
pendingControlRequests: pendingControlRequests(status),
|
|
654
672
|
raw: { ...status },
|
|
655
673
|
};
|
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
queueItems,
|
|
19
19
|
sameRuntime,
|
|
20
20
|
streamDeltaMessageType,
|
|
21
|
+
streamDeltaOtid,
|
|
21
22
|
streamDeltaRecord,
|
|
22
23
|
streamDeltaRunId,
|
|
24
|
+
streamDeltaSeqId,
|
|
23
25
|
streamDeltaStopReason,
|
|
24
26
|
toSdkErrorCode,
|
|
25
27
|
toSessionDeviceStatus,
|
|
@@ -303,6 +305,8 @@ export class RemoteTurnCoordinator {
|
|
|
303
305
|
const messageType =
|
|
304
306
|
typeof delta.message_type === "string" ? delta.message_type : undefined;
|
|
305
307
|
const runId = typeof delta.run_id === "string" ? delta.run_id : undefined;
|
|
308
|
+
const otid = streamDeltaOtid(delta);
|
|
309
|
+
const seqId = streamDeltaSeqId(delta);
|
|
306
310
|
const uuid =
|
|
307
311
|
typeof delta.id === "string"
|
|
308
312
|
? delta.id
|
|
@@ -312,7 +316,14 @@ export class RemoteTurnCoordinator {
|
|
|
312
316
|
const content = extractTextFromContent(delta.content);
|
|
313
317
|
if (!content) return null;
|
|
314
318
|
if (this.activeTurn) this.activeTurn.assistantText += content;
|
|
315
|
-
return {
|
|
319
|
+
return {
|
|
320
|
+
type: "assistant",
|
|
321
|
+
content,
|
|
322
|
+
uuid,
|
|
323
|
+
...(otid !== undefined ? { otid } : {}),
|
|
324
|
+
...(seqId !== undefined ? { seqId } : {}),
|
|
325
|
+
runId,
|
|
326
|
+
};
|
|
316
327
|
}
|
|
317
328
|
|
|
318
329
|
if (messageType === "reasoning_message") {
|
|
@@ -321,7 +332,14 @@ export class RemoteTurnCoordinator {
|
|
|
321
332
|
? delta.reasoning
|
|
322
333
|
: extractTextFromContent(delta.content);
|
|
323
334
|
if (!content) return null;
|
|
324
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
type: "reasoning",
|
|
337
|
+
content,
|
|
338
|
+
uuid,
|
|
339
|
+
...(otid !== undefined ? { otid } : {}),
|
|
340
|
+
...(seqId !== undefined ? { seqId } : {}),
|
|
341
|
+
runId,
|
|
342
|
+
};
|
|
325
343
|
}
|
|
326
344
|
|
|
327
345
|
if (
|