@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,405 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAppServerClient,
|
|
3
|
+
type AppServerClient,
|
|
4
|
+
type AppServerRequestCommandWithId,
|
|
5
|
+
type AppServerSocketConstructor,
|
|
6
|
+
} from "@letta-ai/letta-code/app-server-client";
|
|
7
|
+
import type {
|
|
8
|
+
ManagementQuery,
|
|
9
|
+
ManagementTransport,
|
|
10
|
+
} from "./management.js";
|
|
11
|
+
import { applyUniqueRequestIds } from "./request-ids.js";
|
|
12
|
+
import type {
|
|
13
|
+
ConversationMessagesResult,
|
|
14
|
+
LettaAgent,
|
|
15
|
+
LettaConversation,
|
|
16
|
+
} from "./management-types.js";
|
|
17
|
+
import type {
|
|
18
|
+
LettaCodeModelEntry,
|
|
19
|
+
LettaCodeRemoteClientOptions,
|
|
20
|
+
ListModelsResult,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
type OwnedConnection = { url: string; close(): void };
|
|
24
|
+
|
|
25
|
+
type ManagementResponse = {
|
|
26
|
+
type: string;
|
|
27
|
+
success: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type AgentListResponse = ManagementResponse & {
|
|
32
|
+
agents: LettaAgent[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type AgentResponse = ManagementResponse & {
|
|
36
|
+
agent: LettaAgent | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ConversationListResponse = ManagementResponse & {
|
|
40
|
+
conversations: LettaConversation[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type ConversationResponse = ManagementResponse & {
|
|
44
|
+
conversation: LettaConversation | null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type ConversationMessagesResponse = ManagementResponse & {
|
|
48
|
+
messages: Record<string, unknown>[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type ListModelsResponse = ManagementResponse & {
|
|
52
|
+
entries?: unknown;
|
|
53
|
+
available_handles?: unknown;
|
|
54
|
+
byok_provider_aliases?: unknown;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type AppServerManagementOptions =
|
|
58
|
+
Partial<LettaCodeRemoteClientOptions> & {
|
|
59
|
+
url?: string;
|
|
60
|
+
connect?: () => Promise<OwnedConnection>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type ActiveConnection = {
|
|
64
|
+
client: AppServerClient;
|
|
65
|
+
ownedConnection: OwnedConnection | null;
|
|
66
|
+
detachDisconnect: () => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function ensureResponse<T>(
|
|
70
|
+
response: { success: boolean; error?: string },
|
|
71
|
+
value: T | null | undefined,
|
|
72
|
+
fallback: string,
|
|
73
|
+
): T {
|
|
74
|
+
if (!response.success || value == null) {
|
|
75
|
+
throw new Error(response.error ?? fallback);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function modelEntries(value: unknown): LettaCodeModelEntry[] {
|
|
81
|
+
if (!Array.isArray(value)) return [];
|
|
82
|
+
return value.flatMap((entry) => {
|
|
83
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
84
|
+
const record = entry as Record<string, unknown>;
|
|
85
|
+
if (
|
|
86
|
+
typeof record.id !== "string" ||
|
|
87
|
+
typeof record.handle !== "string" ||
|
|
88
|
+
typeof record.label !== "string" ||
|
|
89
|
+
typeof record.description !== "string"
|
|
90
|
+
) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
return [{
|
|
94
|
+
...record,
|
|
95
|
+
id: record.id,
|
|
96
|
+
handle: record.handle,
|
|
97
|
+
label: record.label,
|
|
98
|
+
description: record.description,
|
|
99
|
+
}];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stringRecord(value: unknown): Record<string, string> | undefined {
|
|
104
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const entries = Object.entries(value as Record<string, unknown>).filter(
|
|
108
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
109
|
+
);
|
|
110
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Management transport that speaks the app-server control protocol.
|
|
115
|
+
*
|
|
116
|
+
* The app-server assigns every client an independent connection, so management
|
|
117
|
+
* keeps one lazy pooled client while sessions connect alongside it. Unexpected
|
|
118
|
+
* disconnects discard the pool and the next management request reconnects.
|
|
119
|
+
*/
|
|
120
|
+
export class AppServerManagementTransport
|
|
121
|
+
implements ManagementTransport
|
|
122
|
+
{
|
|
123
|
+
private connectionPromise: Promise<ActiveConnection> | null = null;
|
|
124
|
+
private closingConnections = new Set<Promise<void>>();
|
|
125
|
+
|
|
126
|
+
constructor(private readonly options: AppServerManagementOptions) {}
|
|
127
|
+
|
|
128
|
+
async listAgents(query: ManagementQuery): Promise<LettaAgent[]> {
|
|
129
|
+
const response = await this.request<AgentListResponse>(
|
|
130
|
+
"agent_list",
|
|
131
|
+
{ query },
|
|
132
|
+
"agent_list_response",
|
|
133
|
+
);
|
|
134
|
+
if (!response.success) {
|
|
135
|
+
throw new Error(response.error ?? "Failed to list agents.");
|
|
136
|
+
}
|
|
137
|
+
return response.agents;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async retrieveAgent(agentId: string): Promise<LettaAgent> {
|
|
141
|
+
const response = await this.request<AgentResponse>(
|
|
142
|
+
"agent_retrieve",
|
|
143
|
+
{ agent_id: agentId },
|
|
144
|
+
"agent_retrieve_response",
|
|
145
|
+
);
|
|
146
|
+
return ensureResponse(
|
|
147
|
+
response,
|
|
148
|
+
response.agent,
|
|
149
|
+
`Failed to retrieve agent ${agentId}.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async updateAgent(
|
|
154
|
+
agentId: string,
|
|
155
|
+
body: Record<string, unknown>,
|
|
156
|
+
): Promise<LettaAgent> {
|
|
157
|
+
const response = await this.request<AgentResponse>(
|
|
158
|
+
"agent_update",
|
|
159
|
+
{ agent_id: agentId, body },
|
|
160
|
+
"agent_update_response",
|
|
161
|
+
);
|
|
162
|
+
return ensureResponse(
|
|
163
|
+
response,
|
|
164
|
+
response.agent,
|
|
165
|
+
`Failed to update agent ${agentId}.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async deleteAgent(agentId: string): Promise<void> {
|
|
170
|
+
const response = await this.request<ManagementResponse>(
|
|
171
|
+
"agent_delete",
|
|
172
|
+
{ agent_id: agentId },
|
|
173
|
+
"agent_delete_response",
|
|
174
|
+
);
|
|
175
|
+
if (!response.success) {
|
|
176
|
+
throw new Error(response.error ?? `Failed to delete agent ${agentId}.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async listModels(): Promise<ListModelsResult> {
|
|
181
|
+
// A bare list_models command (no runtime scope) is answered on the
|
|
182
|
+
// control channel, so no session or conversation is required.
|
|
183
|
+
const response = await this.request<ListModelsResponse>(
|
|
184
|
+
"list_models",
|
|
185
|
+
{},
|
|
186
|
+
"list_models_response",
|
|
187
|
+
);
|
|
188
|
+
if (!response.success) {
|
|
189
|
+
throw new Error(response.error ?? "Failed to list models.");
|
|
190
|
+
}
|
|
191
|
+
const result: ListModelsResult = {
|
|
192
|
+
entries: modelEntries(response.entries),
|
|
193
|
+
};
|
|
194
|
+
if (response.available_handles === null) {
|
|
195
|
+
result.availableHandles = null;
|
|
196
|
+
} else if (Array.isArray(response.available_handles)) {
|
|
197
|
+
result.availableHandles = response.available_handles.filter(
|
|
198
|
+
(handle): handle is string => typeof handle === "string",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const aliases = stringRecord(response.byok_provider_aliases);
|
|
202
|
+
if (aliases) result.byokProviderAliases = aliases;
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async listConversations(
|
|
207
|
+
query: ManagementQuery,
|
|
208
|
+
): Promise<LettaConversation[]> {
|
|
209
|
+
const response = await this.request<ConversationListResponse>(
|
|
210
|
+
"conversation_list",
|
|
211
|
+
{ query },
|
|
212
|
+
"conversation_list_response",
|
|
213
|
+
);
|
|
214
|
+
if (!response.success) {
|
|
215
|
+
throw new Error(response.error ?? "Failed to list conversations.");
|
|
216
|
+
}
|
|
217
|
+
return response.conversations;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async retrieveConversation(
|
|
221
|
+
conversationId: string,
|
|
222
|
+
): Promise<LettaConversation> {
|
|
223
|
+
const response = await this.request<ConversationResponse>(
|
|
224
|
+
"conversation_retrieve",
|
|
225
|
+
{ conversation_id: conversationId },
|
|
226
|
+
"conversation_retrieve_response",
|
|
227
|
+
);
|
|
228
|
+
return ensureResponse(
|
|
229
|
+
response,
|
|
230
|
+
response.conversation,
|
|
231
|
+
`Failed to retrieve conversation ${conversationId}.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async createConversation(
|
|
236
|
+
body: Record<string, unknown>,
|
|
237
|
+
): Promise<LettaConversation> {
|
|
238
|
+
const response = await this.request<ConversationResponse>(
|
|
239
|
+
"conversation_create",
|
|
240
|
+
{ body },
|
|
241
|
+
"conversation_create_response",
|
|
242
|
+
);
|
|
243
|
+
return ensureResponse(
|
|
244
|
+
response,
|
|
245
|
+
response.conversation,
|
|
246
|
+
"Failed to create conversation.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async updateConversation(
|
|
251
|
+
conversationId: string,
|
|
252
|
+
body: Record<string, unknown>,
|
|
253
|
+
): Promise<LettaConversation> {
|
|
254
|
+
const response = await this.request<ConversationResponse>(
|
|
255
|
+
"conversation_update",
|
|
256
|
+
{ conversation_id: conversationId, body },
|
|
257
|
+
"conversation_update_response",
|
|
258
|
+
);
|
|
259
|
+
return ensureResponse(
|
|
260
|
+
response,
|
|
261
|
+
response.conversation,
|
|
262
|
+
`Failed to update conversation ${conversationId}.`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async listConversationMessages(
|
|
267
|
+
conversationId: string,
|
|
268
|
+
query: ManagementQuery,
|
|
269
|
+
): Promise<ConversationMessagesResult> {
|
|
270
|
+
const response =
|
|
271
|
+
await this.request<ConversationMessagesResponse>(
|
|
272
|
+
"conversation_messages_list",
|
|
273
|
+
{ conversation_id: conversationId, query },
|
|
274
|
+
"conversation_messages_list_response",
|
|
275
|
+
);
|
|
276
|
+
if (!response.success) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
response.error ??
|
|
279
|
+
`Failed to list messages for conversation ${conversationId}.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return { messages: response.messages };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private async request<TResponse extends { type: string }>(
|
|
286
|
+
type: string,
|
|
287
|
+
body: Record<string, unknown>,
|
|
288
|
+
responseType: string,
|
|
289
|
+
): Promise<TResponse> {
|
|
290
|
+
if (this.closingConnections.size > 0) {
|
|
291
|
+
await Promise.all([...this.closingConnections]);
|
|
292
|
+
}
|
|
293
|
+
// Concurrent requests share the same connect promise and request-id
|
|
294
|
+
// counter, while connection identity keeps this pool independent from
|
|
295
|
+
// session clients using the same app-server.
|
|
296
|
+
const { client } = await this.acquireConnection();
|
|
297
|
+
const command = {
|
|
298
|
+
type,
|
|
299
|
+
request_id: client.nextRequestId(type),
|
|
300
|
+
...body,
|
|
301
|
+
} as AppServerRequestCommandWithId;
|
|
302
|
+
const response = await client.request(command, {
|
|
303
|
+
predicate: (message): message is typeof message =>
|
|
304
|
+
message.type === responseType,
|
|
305
|
+
});
|
|
306
|
+
return response as unknown as TResponse;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private acquireConnection(): Promise<ActiveConnection> {
|
|
310
|
+
if (this.connectionPromise) return this.connectionPromise;
|
|
311
|
+
const promise: Promise<ActiveConnection> = this.openConnection().then(
|
|
312
|
+
(connection) => {
|
|
313
|
+
// Unexpected disconnects (explicit closes do not notify) drop the
|
|
314
|
+
// pooled connection so the next request reconnects lazily.
|
|
315
|
+
connection.detachDisconnect = connection.client.onDisconnect(() => {
|
|
316
|
+
this.discardConnection(promise, connection);
|
|
317
|
+
});
|
|
318
|
+
return connection;
|
|
319
|
+
},
|
|
320
|
+
(error) => {
|
|
321
|
+
if (this.connectionPromise === promise) {
|
|
322
|
+
this.connectionPromise = null;
|
|
323
|
+
}
|
|
324
|
+
throw error;
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
this.connectionPromise = promise;
|
|
328
|
+
return promise;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async openConnection(): Promise<ActiveConnection> {
|
|
332
|
+
const ownedConnection = this.options.url
|
|
333
|
+
? null
|
|
334
|
+
: ((await this.options.connect?.()) ?? null);
|
|
335
|
+
const url = this.options.url ?? ownedConnection?.url;
|
|
336
|
+
if (!url) {
|
|
337
|
+
throw new Error("App-server management requires a url or connect hook.");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let client: AppServerClient | null = null;
|
|
341
|
+
try {
|
|
342
|
+
client = applyUniqueRequestIds(createAppServerClient({
|
|
343
|
+
url,
|
|
344
|
+
...(this.options.authToken !== undefined
|
|
345
|
+
? { authToken: this.options.authToken }
|
|
346
|
+
: {}),
|
|
347
|
+
...(this.options.WebSocket
|
|
348
|
+
? {
|
|
349
|
+
WebSocket:
|
|
350
|
+
this.options.WebSocket as AppServerSocketConstructor,
|
|
351
|
+
}
|
|
352
|
+
: {}),
|
|
353
|
+
...(this.options.requestTimeoutMs !== undefined
|
|
354
|
+
? { requestTimeoutMs: this.options.requestTimeoutMs }
|
|
355
|
+
: {}),
|
|
356
|
+
}));
|
|
357
|
+
await client.connect();
|
|
358
|
+
} catch (error) {
|
|
359
|
+
try {
|
|
360
|
+
if (client) {
|
|
361
|
+
client.close();
|
|
362
|
+
ownedConnection?.close();
|
|
363
|
+
} else {
|
|
364
|
+
ownedConnection?.close();
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
// Preserve the original connect error after best-effort cleanup.
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
client,
|
|
373
|
+
ownedConnection,
|
|
374
|
+
detachDisconnect: () => {},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private discardConnection(
|
|
379
|
+
promise: Promise<ActiveConnection>,
|
|
380
|
+
connection: ActiveConnection,
|
|
381
|
+
): void {
|
|
382
|
+
if (this.connectionPromise === promise) {
|
|
383
|
+
this.connectionPromise = null;
|
|
384
|
+
}
|
|
385
|
+
void this.trackClosingConnection(connection);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private trackClosingConnection(
|
|
389
|
+
connection: ActiveConnection,
|
|
390
|
+
): Promise<void> {
|
|
391
|
+
const closing = closeConnection(connection);
|
|
392
|
+
this.closingConnections.add(closing);
|
|
393
|
+
void closing.then(
|
|
394
|
+
() => this.closingConnections.delete(closing),
|
|
395
|
+
() => this.closingConnections.delete(closing),
|
|
396
|
+
);
|
|
397
|
+
return closing;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function closeConnection(connection: ActiveConnection): Promise<void> {
|
|
402
|
+
connection.detachDisconnect();
|
|
403
|
+
connection.client.close();
|
|
404
|
+
connection.ownedConnection?.close();
|
|
405
|
+
}
|