@letta-ai/letta-agent-sdk 0.3.2 → 0.3.3
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 +47 -0
- package/README.md +17 -0
- package/dist/client-entry.js +800 -732
- package/dist/client-entry.js.map +8 -5
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +799 -732
- package/dist/index.js.map +9 -6
- 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 +130 -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 +2 -20
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/app-server-management.ts +641 -0
- package/src/app-server-session.ts +948 -0
- package/src/cli-resolver.ts +46 -0
- package/src/client-base.ts +482 -0
- package/src/client-entry.ts +31 -0
- package/src/client.ts +138 -0
- package/src/cloud-management.ts +360 -0
- package/src/cloud-sandbox.ts +117 -0
- package/src/cloud-session.ts +1313 -0
- package/src/index.ts +440 -0
- package/src/interactiveToolPolicy.ts +62 -0
- package/src/local-app-server-session.ts +39 -0
- package/src/local-app-server.ts +137 -0
- package/src/management-types.ts +133 -0
- package/src/management.ts +206 -0
- package/src/protocol.ts +249 -0
- package/src/remote-client-session-core.ts +786 -0
- package/src/remote-session-protocol.ts +660 -0
- package/src/remote-turn-coordinator.ts +505 -0
- package/src/remote.ts +177 -0
- package/src/repositories.ts +340 -0
- package/src/request-ids.ts +33 -0
- package/src/session.ts +1638 -0
- package/src/stream-events.ts +88 -0
- package/src/tool-helpers.ts +147 -0
- package/src/transport.ts +484 -0
- package/src/types.ts +1328 -0
- package/src/validation.ts +223 -0
- package/src/websocket.ts +22 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateRepositoryFileParams,
|
|
3
|
+
CreateRepositoryParams,
|
|
4
|
+
DeleteRepositoryFileParams,
|
|
5
|
+
DeleteRepositoryFileResult,
|
|
6
|
+
GetRepositoryVersionParams,
|
|
7
|
+
LettaCodeCloudClientOptions,
|
|
8
|
+
ListRepositoriesParams,
|
|
9
|
+
ListRepositoriesResult,
|
|
10
|
+
ListRepositoryFilesParams,
|
|
11
|
+
ListRepositoryFilesResult,
|
|
12
|
+
ListRepositoryVersionsParams,
|
|
13
|
+
Repository,
|
|
14
|
+
RepositoryFile,
|
|
15
|
+
RepositoryFileMutationResult,
|
|
16
|
+
RepositoryVersion,
|
|
17
|
+
UpdateRepositoryFileParams,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CLOUD_API_BASE_URL = "https://api.letta.com";
|
|
21
|
+
|
|
22
|
+
type FetchLike = typeof fetch;
|
|
23
|
+
|
|
24
|
+
function getDefaultApiKey(): string | undefined {
|
|
25
|
+
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })
|
|
26
|
+
.process?.env;
|
|
27
|
+
return env?.LETTA_API_KEY ?? env?.LETTA_CLOUD_API_KEY;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function bearerTokenFromHeaders(headers: Record<string, string> | undefined): string | undefined {
|
|
31
|
+
const authorization = headers?.Authorization ?? headers?.authorization;
|
|
32
|
+
if (!authorization) return undefined;
|
|
33
|
+
const match = /^Bearer\s+(.+)$/i.exec(authorization);
|
|
34
|
+
return match?.[1];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getCloudApiKey(options: LettaCodeCloudClientOptions): string | undefined {
|
|
38
|
+
return options.apiKey ?? bearerTokenFromHeaders(options.headers) ?? getDefaultApiKey();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getFetch(fetchOverride?: FetchLike): FetchLike {
|
|
42
|
+
const resolved = fetchOverride ?? globalThis.fetch;
|
|
43
|
+
if (!resolved) throw new Error("No fetch implementation available for cloud backend.");
|
|
44
|
+
return resolved.bind(globalThis) as FetchLike;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeCloudApiBaseUrl(url: string | undefined): string {
|
|
48
|
+
const parsed = new URL(url ?? DEFAULT_CLOUD_API_BASE_URL);
|
|
49
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
50
|
+
parsed.search = "";
|
|
51
|
+
parsed.hash = "";
|
|
52
|
+
return parsed.toString().replace(/\/$/, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cloudHeaders(options: LettaCodeCloudClientOptions): Record<string, string> {
|
|
56
|
+
const headers: Record<string, string> = {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
...(options.headers ?? {}),
|
|
59
|
+
};
|
|
60
|
+
const apiKey = getCloudApiKey(options);
|
|
61
|
+
if (apiKey && !headers.Authorization && !headers.authorization) {
|
|
62
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
63
|
+
}
|
|
64
|
+
return headers;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function parseJsonResponse(response: Response): Promise<unknown> {
|
|
68
|
+
const text = await response.text();
|
|
69
|
+
if (!text) return null;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(text) as unknown;
|
|
72
|
+
} catch {
|
|
73
|
+
return text;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function responseErrorMessage(body: unknown, fallback: string): string {
|
|
78
|
+
if (body && typeof body === "object") {
|
|
79
|
+
const record = body as Record<string, unknown>;
|
|
80
|
+
const message = record.message ?? record.error ?? record.detail;
|
|
81
|
+
const reasonText = record.reason_text;
|
|
82
|
+
const pieces = [message, reasonText]
|
|
83
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0);
|
|
84
|
+
if (pieces.length > 0) return pieces.join(": ");
|
|
85
|
+
}
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertOkResponse(response: Response, body: unknown, action: string): void {
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(responseErrorMessage(body, `${action} failed with HTTP ${response.status}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function optionalString(value: unknown): string | undefined {
|
|
96
|
+
return typeof value === "string" ? value : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requireString(value: unknown, name: string): string {
|
|
100
|
+
if (typeof value !== "string") throw new Error(`Cloud repositories response missing ${name}.`);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toRepository(body: unknown): Repository {
|
|
105
|
+
if (!body || typeof body !== "object") {
|
|
106
|
+
throw new Error("Cloud repositories response did not include a repository.");
|
|
107
|
+
}
|
|
108
|
+
const record = body as Record<string, unknown>;
|
|
109
|
+
return {
|
|
110
|
+
id: requireString(record.id, "id"),
|
|
111
|
+
name: requireString(record.name, "name"),
|
|
112
|
+
createdAt: requireString(record.created_at, "created_at"),
|
|
113
|
+
updatedAt: requireString(record.updated_at, "updated_at"),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toFileMutation(body: unknown): RepositoryFileMutationResult {
|
|
118
|
+
if (!body || typeof body !== "object") {
|
|
119
|
+
throw new Error("Cloud repositories file response did not include file details.");
|
|
120
|
+
}
|
|
121
|
+
const record = body as Record<string, unknown>;
|
|
122
|
+
return {
|
|
123
|
+
path: requireString(record.path, "path"),
|
|
124
|
+
contentSha256: requireString(record.content_sha256, "content_sha256"),
|
|
125
|
+
commitSha: requireString(record.commit_sha, "commit_sha"),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function addOptionalSearchParam(url: URL, key: string, value: string | number | undefined): void {
|
|
130
|
+
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class RepositoriesClient {
|
|
134
|
+
constructor(private readonly options: LettaCodeCloudClientOptions) {}
|
|
135
|
+
|
|
136
|
+
async create(params: CreateRepositoryParams): Promise<Repository> {
|
|
137
|
+
return toRepository(await this.request("/v1/repositories", "POST", { name: params.name }, "Cloud create repository"));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async list(params: ListRepositoriesParams = {}): Promise<ListRepositoriesResult> {
|
|
141
|
+
const url = this.url("/v1/repositories");
|
|
142
|
+
addOptionalSearchParam(url, "limit", params.limit);
|
|
143
|
+
addOptionalSearchParam(url, "offset", params.offset);
|
|
144
|
+
const body = await this.requestUrl(url, "GET", undefined, "Cloud list repositories");
|
|
145
|
+
if (!body || typeof body !== "object") {
|
|
146
|
+
throw new Error("Cloud list repositories response did not include repositories.");
|
|
147
|
+
}
|
|
148
|
+
const record = body as Record<string, unknown>;
|
|
149
|
+
const repositories = Array.isArray(record.repositories)
|
|
150
|
+
? record.repositories.map(toRepository)
|
|
151
|
+
: [];
|
|
152
|
+
return {
|
|
153
|
+
repositories,
|
|
154
|
+
hasNextPage: record.has_next_page === true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async get(repositoryId: string): Promise<Repository> {
|
|
159
|
+
return toRepository(await this.request(`/v1/repositories/${encodeURIComponent(repositoryId)}`, "GET", undefined, "Cloud get repository"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Delete a repository. The server soft-deletes it (marks the repository
|
|
164
|
+
* deleted rather than removing its data). Resolves on success; throws if the
|
|
165
|
+
* repository does not exist or is not visible to the caller's organization.
|
|
166
|
+
*/
|
|
167
|
+
async delete(repositoryId: string): Promise<void> {
|
|
168
|
+
await this.request(
|
|
169
|
+
`/v1/repositories/${encodeURIComponent(repositoryId)}`,
|
|
170
|
+
"DELETE",
|
|
171
|
+
undefined,
|
|
172
|
+
"Cloud delete repository",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
readonly files = {
|
|
177
|
+
list: async (
|
|
178
|
+
repositoryId: string,
|
|
179
|
+
params: ListRepositoryFilesParams = {},
|
|
180
|
+
): Promise<ListRepositoryFilesResult> => {
|
|
181
|
+
const url = this.url(`/v1/repositories/${encodeURIComponent(repositoryId)}/files`);
|
|
182
|
+
addOptionalSearchParam(url, "path_prefix", params.pathPrefix);
|
|
183
|
+
addOptionalSearchParam(url, "depth", params.depth);
|
|
184
|
+
addOptionalSearchParam(url, "ref", params.ref);
|
|
185
|
+
const body = await this.requestUrl(url, "GET", undefined, "Cloud list repository files");
|
|
186
|
+
if (!body || typeof body !== "object") {
|
|
187
|
+
throw new Error("Cloud list repository files response did not include files.");
|
|
188
|
+
}
|
|
189
|
+
const record = body as Record<string, unknown>;
|
|
190
|
+
const files = Array.isArray(record.files)
|
|
191
|
+
? record.files.map((entry) => {
|
|
192
|
+
if (!entry || typeof entry !== "object") {
|
|
193
|
+
throw new Error("Cloud list repository files response included an invalid file entry.");
|
|
194
|
+
}
|
|
195
|
+
const file = entry as Record<string, unknown>;
|
|
196
|
+
const type = file.type;
|
|
197
|
+
if (type !== "file" && type !== "directory") {
|
|
198
|
+
throw new Error("Cloud list repository files response included an invalid file type.");
|
|
199
|
+
}
|
|
200
|
+
return { path: requireString(file.path, "path"), type: type as "file" | "directory" };
|
|
201
|
+
})
|
|
202
|
+
: [];
|
|
203
|
+
return { files, ref: requireString(record.ref, "ref") };
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
create: async (
|
|
207
|
+
repositoryId: string,
|
|
208
|
+
params: CreateRepositoryFileParams,
|
|
209
|
+
): Promise<RepositoryFileMutationResult> => toFileMutation(await this.request(
|
|
210
|
+
`/v1/repositories/${encodeURIComponent(repositoryId)}/files`,
|
|
211
|
+
"POST",
|
|
212
|
+
{ path: params.path, content: params.content },
|
|
213
|
+
"Cloud create repository file",
|
|
214
|
+
)),
|
|
215
|
+
|
|
216
|
+
read: async (repositoryId: string, params: { path: string; ref?: string }): Promise<RepositoryFile> => {
|
|
217
|
+
const url = this.url(`/v1/repositories/${encodeURIComponent(repositoryId)}/files/content`);
|
|
218
|
+
url.searchParams.set("path", params.path);
|
|
219
|
+
addOptionalSearchParam(url, "ref", params.ref);
|
|
220
|
+
const body = await this.requestUrl(url, "GET", undefined, "Cloud read repository file");
|
|
221
|
+
if (!body || typeof body !== "object") {
|
|
222
|
+
throw new Error("Cloud read repository file response did not include file content.");
|
|
223
|
+
}
|
|
224
|
+
const record = body as Record<string, unknown>;
|
|
225
|
+
return {
|
|
226
|
+
path: requireString(record.path, "path"),
|
|
227
|
+
content: requireString(record.content, "content"),
|
|
228
|
+
contentSha256: requireString(record.content_sha256, "content_sha256"),
|
|
229
|
+
ref: optionalString(record.ref),
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
update: async (
|
|
234
|
+
repositoryId: string,
|
|
235
|
+
params: UpdateRepositoryFileParams,
|
|
236
|
+
): Promise<RepositoryFileMutationResult> => toFileMutation(await this.request(
|
|
237
|
+
`/v1/repositories/${encodeURIComponent(repositoryId)}/files/content`,
|
|
238
|
+
"POST",
|
|
239
|
+
{
|
|
240
|
+
path: params.path,
|
|
241
|
+
...(params.content !== undefined ? { content: params.content } : {}),
|
|
242
|
+
...(params.newPath !== undefined ? { new_path: params.newPath } : {}),
|
|
243
|
+
...(params.precondition !== undefined
|
|
244
|
+
? {
|
|
245
|
+
precondition: {
|
|
246
|
+
type: "content_sha256",
|
|
247
|
+
content_sha256: params.precondition.contentSha256,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
: {}),
|
|
251
|
+
},
|
|
252
|
+
"Cloud update repository file",
|
|
253
|
+
)),
|
|
254
|
+
|
|
255
|
+
delete: async (
|
|
256
|
+
repositoryId: string,
|
|
257
|
+
params: DeleteRepositoryFileParams,
|
|
258
|
+
): Promise<DeleteRepositoryFileResult> => {
|
|
259
|
+
const body = await this.request(
|
|
260
|
+
`/v1/repositories/${encodeURIComponent(repositoryId)}/files/content`,
|
|
261
|
+
"DELETE",
|
|
262
|
+
{ path: params.path },
|
|
263
|
+
"Cloud delete repository file",
|
|
264
|
+
);
|
|
265
|
+
if (!body || typeof body !== "object") {
|
|
266
|
+
throw new Error("Cloud delete repository file response did not include delete details.");
|
|
267
|
+
}
|
|
268
|
+
const record = body as Record<string, unknown>;
|
|
269
|
+
return { success: record.success === true, commitSha: requireString(record.commit_sha, "commit_sha") };
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
readonly versions = {
|
|
274
|
+
list: async (
|
|
275
|
+
repositoryId: string,
|
|
276
|
+
params: ListRepositoryVersionsParams = {},
|
|
277
|
+
): Promise<RepositoryVersion[]> => {
|
|
278
|
+
const url = this.url(`/v1/repositories/${encodeURIComponent(repositoryId)}/versions`);
|
|
279
|
+
addOptionalSearchParam(url, "path", params.path);
|
|
280
|
+
addOptionalSearchParam(url, "limit", params.limit);
|
|
281
|
+
const body = await this.requestUrl(url, "GET", undefined, "Cloud list repository versions");
|
|
282
|
+
if (Array.isArray(body)) return body as RepositoryVersion[];
|
|
283
|
+
if (body && typeof body === "object") {
|
|
284
|
+
const record = body as Record<string, unknown>;
|
|
285
|
+
if (Array.isArray(record.commits)) return record.commits as RepositoryVersion[];
|
|
286
|
+
if (Array.isArray(record.versions)) return record.versions as RepositoryVersion[];
|
|
287
|
+
}
|
|
288
|
+
return [];
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
get: async (
|
|
292
|
+
repositoryId: string,
|
|
293
|
+
sha: string,
|
|
294
|
+
params: GetRepositoryVersionParams,
|
|
295
|
+
): Promise<RepositoryFile> => {
|
|
296
|
+
const url = this.url(`/v1/repositories/${encodeURIComponent(repositoryId)}/versions/${encodeURIComponent(sha)}`);
|
|
297
|
+
url.searchParams.set("path", params.path);
|
|
298
|
+
const body = await this.requestUrl(url, "GET", undefined, "Cloud get repository version");
|
|
299
|
+
if (!body || typeof body !== "object") {
|
|
300
|
+
throw new Error("Cloud get repository version response did not include file content.");
|
|
301
|
+
}
|
|
302
|
+
const record = body as Record<string, unknown>;
|
|
303
|
+
return {
|
|
304
|
+
path: requireString(record.path, "path"),
|
|
305
|
+
content: requireString(record.content, "content"),
|
|
306
|
+
contentSha256: requireString(record.content_sha256, "content_sha256"),
|
|
307
|
+
ref: requireString(record.sha, "sha"),
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
private url(path: string): URL {
|
|
313
|
+
return new URL(`${normalizeCloudApiBaseUrl(this.options.apiBaseUrl)}${path}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async request(
|
|
317
|
+
path: string,
|
|
318
|
+
method: string,
|
|
319
|
+
body: unknown,
|
|
320
|
+
action: string,
|
|
321
|
+
): Promise<unknown> {
|
|
322
|
+
return this.requestUrl(this.url(path), method, body, action);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async requestUrl(
|
|
326
|
+
url: URL,
|
|
327
|
+
method: string,
|
|
328
|
+
body: unknown,
|
|
329
|
+
action: string,
|
|
330
|
+
): Promise<unknown> {
|
|
331
|
+
const response = await getFetch(this.options.fetch)(url, {
|
|
332
|
+
method,
|
|
333
|
+
headers: cloudHeaders(this.options),
|
|
334
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
335
|
+
});
|
|
336
|
+
const parsed = await parseJsonResponse(response);
|
|
337
|
+
assertOkResponse(response, parsed, action);
|
|
338
|
+
return parsed;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collision-free request-id generation for request/response correlation.
|
|
3
|
+
*
|
|
4
|
+
* AppServerClient's built-in nextRequestId() is a bare per-instance counter
|
|
5
|
+
* that restarts at 1, so two client instances (for example two session
|
|
6
|
+
* objects, or a session plus a management request) each emit ids like
|
|
7
|
+
* `conversation_retrieve-1`. When more than one connection is involved the
|
|
8
|
+
* app-server can deliver request-correlated responses where both ids look
|
|
9
|
+
* identical, and the wrong caller's pending request resolves (or times out).
|
|
10
|
+
*
|
|
11
|
+
* Ids generated here embed a per-client random nonce plus a process-wide
|
|
12
|
+
* monotonic counter, so no two clients in the same process can ever emit the
|
|
13
|
+
* same id, and cross-process collisions are vanishingly unlikely.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
let processRequestCounter = 0;
|
|
17
|
+
|
|
18
|
+
/** Create a request-id generator scoped to one client/connection instance. */
|
|
19
|
+
export function createRequestIdGenerator(): (prefix?: string) => string {
|
|
20
|
+
const nonce = Math.random().toString(36).slice(2, 10);
|
|
21
|
+
return (prefix = "req") => `${prefix}-${nonce}-${++processRequestCounter}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Replace a client's request-id generator with a collision-free one.
|
|
26
|
+
* Returns the same client for call-site convenience.
|
|
27
|
+
*/
|
|
28
|
+
export function applyUniqueRequestIds<
|
|
29
|
+
TClient extends { nextRequestId(prefix?: string): string },
|
|
30
|
+
>(client: TClient): TClient {
|
|
31
|
+
client.nextRequestId = createRequestIdGenerator();
|
|
32
|
+
return client;
|
|
33
|
+
}
|