@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Letta Agent SDK
|
|
3
|
+
*
|
|
4
|
+
* Programmatic control of Letta Code CLI with persistent agent memory.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { LettaAgentClient, createAgent, createSession, resumeSession, prompt } from '@letta-ai/letta-agent-sdk';
|
|
9
|
+
*
|
|
10
|
+
* const client = new LettaAgentClient({ backend: 'local' });
|
|
11
|
+
* const agentId = await client.createAgent();
|
|
12
|
+
* const clientSession = client.resumeSession(agentId);
|
|
13
|
+
*
|
|
14
|
+
* // Start session with default agent + new conversation (like `letta`)
|
|
15
|
+
* const session = createSession();
|
|
16
|
+
*
|
|
17
|
+
* // Create a new agent explicitly
|
|
18
|
+
* const agentId = await createAgent();
|
|
19
|
+
*
|
|
20
|
+
* // Resume default conversation on an agent
|
|
21
|
+
* const session = resumeSession(agentId);
|
|
22
|
+
*
|
|
23
|
+
* // Resume specific conversation
|
|
24
|
+
* const session = resumeSession('conv-xxx');
|
|
25
|
+
*
|
|
26
|
+
* // Create new conversation on specific agent
|
|
27
|
+
* const session = createSession(agentId);
|
|
28
|
+
*
|
|
29
|
+
* // One-shot prompt (uses default agent)
|
|
30
|
+
* const result = await prompt('Hello');
|
|
31
|
+
* const result = await prompt('Hello', agentId); // specific agent
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { Session } from "./session.js";
|
|
36
|
+
import { LettaAgentClient } from "./client.js";
|
|
37
|
+
import type {
|
|
38
|
+
CreateSessionOptions,
|
|
39
|
+
CreateAgentOptions,
|
|
40
|
+
LettaCodeSession,
|
|
41
|
+
SDKInitMessage,
|
|
42
|
+
SDKResultMessage,
|
|
43
|
+
SendMessage,
|
|
44
|
+
} from "./types.js";
|
|
45
|
+
import { validateCreateSessionOptions, validateCreateAgentOptions } from "./validation.js";
|
|
46
|
+
|
|
47
|
+
// Re-export types
|
|
48
|
+
export type {
|
|
49
|
+
CreateSessionOptions,
|
|
50
|
+
CreateAgentOptions,
|
|
51
|
+
LettaCodePersonalityId,
|
|
52
|
+
LettaCodeBackend,
|
|
53
|
+
LettaCodeEnvironment,
|
|
54
|
+
LettaCodeLocalClientOptions,
|
|
55
|
+
LettaCodeLocalAppServerOptions,
|
|
56
|
+
LettaCodeRemoteClientOptions,
|
|
57
|
+
LettaCodeCloudClientOptions,
|
|
58
|
+
LettaCodeCloudSandboxOptions,
|
|
59
|
+
GitHubRepositoryRef,
|
|
60
|
+
LettaCodeClientOptions,
|
|
61
|
+
LettaCodeClientSessionOptions,
|
|
62
|
+
LettaCodeSession,
|
|
63
|
+
LettaCodeSocketLike,
|
|
64
|
+
LettaCodeSocketConstructor,
|
|
65
|
+
LettaCodeReactNativeSocketConstructor,
|
|
66
|
+
SDKMessage,
|
|
67
|
+
SDKInitMessage,
|
|
68
|
+
SDKAssistantMessage,
|
|
69
|
+
SDKToolCallMessage,
|
|
70
|
+
SDKToolResultMessage,
|
|
71
|
+
SDKReasoningMessage,
|
|
72
|
+
SDKResultMessage,
|
|
73
|
+
SDKErrorCode,
|
|
74
|
+
SDKStreamEventMessage,
|
|
75
|
+
SDKStreamEventPayload,
|
|
76
|
+
SDKStreamEventDeltaPayload,
|
|
77
|
+
SDKStreamEventMessagePayload,
|
|
78
|
+
SDKUnknownStreamEventPayload,
|
|
79
|
+
SDKErrorMessage,
|
|
80
|
+
SDKRetryMessage,
|
|
81
|
+
SDKQueueItem,
|
|
82
|
+
SDKQueueUpdateMessage,
|
|
83
|
+
SDKLoopStatusMessage,
|
|
84
|
+
SDKProtocolMessage,
|
|
85
|
+
SDKProtocolCommand,
|
|
86
|
+
SendCommandOptions,
|
|
87
|
+
RunTurnOptions,
|
|
88
|
+
RecoverPendingApprovalsOptions,
|
|
89
|
+
RecoverPendingApprovalsResult,
|
|
90
|
+
ChangeDeviceStateOptions,
|
|
91
|
+
RemoveQueuedMessageResult,
|
|
92
|
+
GetDeviceStatusOptions,
|
|
93
|
+
SessionDeviceStatus,
|
|
94
|
+
SessionPendingControlRequest,
|
|
95
|
+
SessionPermissionSuggestion,
|
|
96
|
+
SessionDiffHunkLine,
|
|
97
|
+
SessionDiffHunk,
|
|
98
|
+
SessionDiffPreview,
|
|
99
|
+
SkillSource,
|
|
100
|
+
DreamingOptions,
|
|
101
|
+
DreamingTrigger,
|
|
102
|
+
DreamingBehavior,
|
|
103
|
+
EffectiveDreamingSettings,
|
|
104
|
+
PermissionMode,
|
|
105
|
+
ReasoningEffort,
|
|
106
|
+
CanUseToolCallback,
|
|
107
|
+
CanUseToolContext,
|
|
108
|
+
CanUseToolPermissionSuggestion,
|
|
109
|
+
CanUseToolResponse,
|
|
110
|
+
CanUseToolResponseAllow,
|
|
111
|
+
CanUseToolResponseDeny,
|
|
112
|
+
// Multimodal content types
|
|
113
|
+
TextContent,
|
|
114
|
+
ImageContent,
|
|
115
|
+
MessageContentItem,
|
|
116
|
+
SendMessage,
|
|
117
|
+
// List messages API
|
|
118
|
+
ListMessagesOptions,
|
|
119
|
+
ListMessagesResult,
|
|
120
|
+
ListModelsResult,
|
|
121
|
+
LettaCodeModelEntry,
|
|
122
|
+
UpdateModelOptions,
|
|
123
|
+
UpdateModelResult,
|
|
124
|
+
Repository,
|
|
125
|
+
CreateRepositoryParams,
|
|
126
|
+
ListRepositoriesParams,
|
|
127
|
+
ListRepositoriesResult,
|
|
128
|
+
RepositoryResource,
|
|
129
|
+
RepositoryFileEntry,
|
|
130
|
+
ListRepositoryFilesParams,
|
|
131
|
+
ListRepositoryFilesResult,
|
|
132
|
+
CreateRepositoryFileParams,
|
|
133
|
+
RepositoryFile,
|
|
134
|
+
UpdateRepositoryFileParams,
|
|
135
|
+
RepositoryFileMutationResult,
|
|
136
|
+
DeleteRepositoryFileParams,
|
|
137
|
+
DeleteRepositoryFileResult,
|
|
138
|
+
RepositoryVersion,
|
|
139
|
+
ListRepositoryVersionsParams,
|
|
140
|
+
GetRepositoryVersionParams,
|
|
141
|
+
// Bootstrap API
|
|
142
|
+
BootstrapStateOptions,
|
|
143
|
+
BootstrapStateResult,
|
|
144
|
+
// Tool types
|
|
145
|
+
AgentTool,
|
|
146
|
+
AgentToolResult,
|
|
147
|
+
AgentToolResultContent,
|
|
148
|
+
AgentToolUpdateCallback,
|
|
149
|
+
AnyAgentTool,
|
|
150
|
+
} from "./types.js";
|
|
151
|
+
export type {
|
|
152
|
+
AgentsClient,
|
|
153
|
+
ConversationsClient,
|
|
154
|
+
LettaAgent,
|
|
155
|
+
LettaConversation,
|
|
156
|
+
LettaConversationMessage,
|
|
157
|
+
ModelsClient,
|
|
158
|
+
ListAgentsOptions,
|
|
159
|
+
UpdateAgentOptions,
|
|
160
|
+
ListConversationsOptions,
|
|
161
|
+
CreateConversationOptions,
|
|
162
|
+
UpdateConversationOptions,
|
|
163
|
+
ConversationMessagesOptions,
|
|
164
|
+
ConversationMessagesResult,
|
|
165
|
+
} from "./management-types.js";
|
|
166
|
+
|
|
167
|
+
export { Session } from "./session.js";
|
|
168
|
+
export { RepositoriesClient } from "./repositories.js";
|
|
169
|
+
export { LettaAgentClient } from "./client.js";
|
|
170
|
+
export { CloudManagedSandboxExpiredError } from "./cloud-session.js";
|
|
171
|
+
export { createReactNativeWebSocketConstructor } from "./websocket.js";
|
|
172
|
+
|
|
173
|
+
export { extractStreamTextDelta } from "./stream-events.js";
|
|
174
|
+
|
|
175
|
+
// Tool helpers
|
|
176
|
+
export {
|
|
177
|
+
jsonResult,
|
|
178
|
+
readStringParam,
|
|
179
|
+
readNumberParam,
|
|
180
|
+
readBooleanParam,
|
|
181
|
+
readStringArrayParam,
|
|
182
|
+
} from "./tool-helpers.js";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a new agent with a default conversation.
|
|
186
|
+
* Returns the agentId which can be used with resumeSession or createSession.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* // Create agent with default settings.
|
|
191
|
+
* const agentId = await createAgent();
|
|
192
|
+
*
|
|
193
|
+
* // Create agent with custom memory
|
|
194
|
+
* const agentId = await createAgent({
|
|
195
|
+
* memory: ['persona', 'project'],
|
|
196
|
+
* persona: 'You are a helpful coding assistant',
|
|
197
|
+
* model: 'claude-sonnet-4',
|
|
198
|
+
* tags: ['project:docs']
|
|
199
|
+
* });
|
|
200
|
+
*
|
|
201
|
+
* // Then resume the default conversation:
|
|
202
|
+
* const session = resumeSession(agentId);
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export async function createAgent(options: CreateAgentOptions = {}): Promise<string> {
|
|
206
|
+
validateCreateAgentOptions(options);
|
|
207
|
+
return new LettaAgentClient().createAgent(options);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a new conversation (session).
|
|
212
|
+
*
|
|
213
|
+
* - Without agentId: uses default/LRU agent with new conversation (like `letta`)
|
|
214
|
+
* - With agentId: creates new conversation on specified agent
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* // New conversation on default agent (like `letta`)
|
|
219
|
+
* await using session = createSession();
|
|
220
|
+
*
|
|
221
|
+
* // New conversation on specific agent
|
|
222
|
+
* await using session = createSession(agentId);
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
export function createSession(
|
|
226
|
+
agentId?: string,
|
|
227
|
+
options: CreateSessionOptions = {},
|
|
228
|
+
): LettaCodeSession {
|
|
229
|
+
validateCreateSessionOptions(options);
|
|
230
|
+
if (agentId) {
|
|
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);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resume an existing session.
|
|
240
|
+
*
|
|
241
|
+
* - Pass an agent ID (agent-xxx) to resume the default conversation
|
|
242
|
+
* - Pass a conversation ID (conv-xxx) to resume a specific conversation
|
|
243
|
+
*
|
|
244
|
+
* The default conversation always exists after createAgent, so you can:
|
|
245
|
+
* `createAgent()` → `resumeSession(agentId)` without needing createSession first.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* // Resume default conversation
|
|
250
|
+
* await using session = resumeSession(agentId);
|
|
251
|
+
*
|
|
252
|
+
* // Resume specific conversation
|
|
253
|
+
* await using session = resumeSession('conv-xxx');
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
export function resumeSession(
|
|
257
|
+
id: string,
|
|
258
|
+
options: CreateSessionOptions = {},
|
|
259
|
+
): LettaCodeSession {
|
|
260
|
+
validateCreateSessionOptions(options);
|
|
261
|
+
return new LettaAgentClient().resumeSession(id, options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* One-shot prompt convenience function.
|
|
266
|
+
*
|
|
267
|
+
* - Without agentId: uses default agent (like `letta -p`), new conversation
|
|
268
|
+
* - With agentId: uses specific agent, new conversation
|
|
269
|
+
* - Uses a short-lived session and returns the final turn result.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* const result = await prompt('What is 2+2?'); // default agent
|
|
274
|
+
* const result = await prompt('What is the capital of France?', agentId); // specific agent
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
type TurnSession = LettaCodeSession & {
|
|
278
|
+
runTurn(message: SendMessage): Promise<SDKResultMessage>;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
type InitializableSession = LettaCodeSession & {
|
|
282
|
+
initialize(): Promise<SDKInitMessage>;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export async function prompt(
|
|
286
|
+
message: string,
|
|
287
|
+
agentId?: string
|
|
288
|
+
): Promise<SDKResultMessage> {
|
|
289
|
+
// Use default agent behavior (like letta -p) when no agentId specified
|
|
290
|
+
const session = agentId
|
|
291
|
+
? createSession(agentId)
|
|
292
|
+
: createSession();
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
return await (session as TurnSession).runTurn(message);
|
|
296
|
+
} finally {
|
|
297
|
+
session.close();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════
|
|
302
|
+
// SESSIONLESS APIs
|
|
303
|
+
// ═══════════════════════════════════════════════════════════════
|
|
304
|
+
|
|
305
|
+
import type { ListMessagesOptions, ListMessagesResult } from "./types.js";
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Fetch conversation messages without requiring a pre-existing session.
|
|
309
|
+
*
|
|
310
|
+
* Creates a transient CLI subprocess, fetches the requested message page, and
|
|
311
|
+
* closes the subprocess. Useful for prefetching conversation histories before
|
|
312
|
+
* opening a full session (e.g. desktop sidebar warm-up).
|
|
313
|
+
*
|
|
314
|
+
* Routing follows the same agent/conversation semantics as session history:
|
|
315
|
+
* - Pass a conv-xxx conversationId to read a specific conversation.
|
|
316
|
+
* - Omit conversationId to read the agent's default conversation.
|
|
317
|
+
*
|
|
318
|
+
* @param agentId - Agent ID to fetch messages for.
|
|
319
|
+
* @param options - Pagination / filtering options (same as ListMessagesOptions).
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* // Prefetch default conversation
|
|
324
|
+
* const { messages } = await listMessagesDirect(agentId);
|
|
325
|
+
*
|
|
326
|
+
* // Prefetch a specific conversation
|
|
327
|
+
* const { messages, hasMore, nextBefore } = await listMessagesDirect(agentId, {
|
|
328
|
+
* conversationId: 'conv-abc',
|
|
329
|
+
* limit: 20,
|
|
330
|
+
* order: 'desc',
|
|
331
|
+
* });
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
export async function listMessagesDirect(
|
|
335
|
+
agentId: string,
|
|
336
|
+
options: ListMessagesOptions = {},
|
|
337
|
+
): Promise<ListMessagesResult> {
|
|
338
|
+
// resumeSession uses --default which maps to the agent's default conversation.
|
|
339
|
+
// The session is transient: we only need it long enough to list messages.
|
|
340
|
+
const session = new LettaAgentClient().resumeSession(agentId, {
|
|
341
|
+
permissionMode: "unrestricted",
|
|
342
|
+
});
|
|
343
|
+
await (session as InitializableSession).initialize();
|
|
344
|
+
try {
|
|
345
|
+
return await session.listMessages(options);
|
|
346
|
+
} finally {
|
|
347
|
+
session.close();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ═══════════════════════════════════════════════════════════════
|
|
352
|
+
// IMAGE HELPERS
|
|
353
|
+
// ═══════════════════════════════════════════════════════════════
|
|
354
|
+
|
|
355
|
+
import { readFileSync } from "node:fs";
|
|
356
|
+
import type { ImageContent } from "./types.js";
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Create image content from a file path.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* await session.send([
|
|
364
|
+
* { type: "text", text: "What's in this image?" },
|
|
365
|
+
* imageFromFile("./screenshot.png")
|
|
366
|
+
* ]);
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
369
|
+
export function imageFromFile(filePath: string): ImageContent {
|
|
370
|
+
const data = readFileSync(filePath).toString("base64");
|
|
371
|
+
const ext = filePath.toLowerCase();
|
|
372
|
+
const media_type: ImageContent["source"]["media_type"] =
|
|
373
|
+
ext.endsWith(".png") ? "image/png"
|
|
374
|
+
: ext.endsWith(".gif") ? "image/gif"
|
|
375
|
+
: ext.endsWith(".webp") ? "image/webp"
|
|
376
|
+
: "image/jpeg";
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
type: "image",
|
|
380
|
+
source: { type: "base64", media_type, data }
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Create image content from base64 data.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```typescript
|
|
389
|
+
* const base64 = fs.readFileSync("image.png").toString("base64");
|
|
390
|
+
* await session.send([
|
|
391
|
+
* { type: "text", text: "Describe this" },
|
|
392
|
+
* imageFromBase64(base64, "image/png")
|
|
393
|
+
* ]);
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
396
|
+
export function imageFromBase64(
|
|
397
|
+
data: string,
|
|
398
|
+
media_type: ImageContent["source"]["media_type"] = "image/png"
|
|
399
|
+
): ImageContent {
|
|
400
|
+
return {
|
|
401
|
+
type: "image",
|
|
402
|
+
source: { type: "base64", media_type, data }
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Create image content from a URL.
|
|
408
|
+
* Fetches the image and converts to base64.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```typescript
|
|
412
|
+
* const img = await imageFromURL("https://example.com/image.png");
|
|
413
|
+
* await session.send([
|
|
414
|
+
* { type: "text", text: "What's this?" },
|
|
415
|
+
* img
|
|
416
|
+
* ]);
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
export async function imageFromURL(url: string): Promise<ImageContent> {
|
|
420
|
+
const response = await fetch(url);
|
|
421
|
+
const buffer = await response.arrayBuffer();
|
|
422
|
+
const data = Buffer.from(buffer).toString("base64");
|
|
423
|
+
|
|
424
|
+
// Detect media type from content-type header or URL
|
|
425
|
+
const contentType = response.headers.get("content-type");
|
|
426
|
+
let media_type: ImageContent["source"]["media_type"] = "image/png";
|
|
427
|
+
|
|
428
|
+
if (contentType?.includes("jpeg") || contentType?.includes("jpg") || url.match(/\.jpe?g$/i)) {
|
|
429
|
+
media_type = "image/jpeg";
|
|
430
|
+
} else if (contentType?.includes("gif") || url.endsWith(".gif")) {
|
|
431
|
+
media_type = "image/gif";
|
|
432
|
+
} else if (contentType?.includes("webp") || url.endsWith(".webp")) {
|
|
433
|
+
media_type = "image/webp";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
type: "image",
|
|
438
|
+
source: { type: "base64", media_type, data }
|
|
439
|
+
};
|
|
440
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Interactive tool policy for SDK permission callbacks.
|
|
2
|
+
// Centralizes behavior so transport/session logic doesn't hardcode names inline.
|
|
3
|
+
|
|
4
|
+
import type { CanUseToolContext, CanUseToolPermissionSuggestion } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const INTERACTIVE_APPROVAL_TOOLS = new Set([
|
|
7
|
+
"AskUserQuestion",
|
|
8
|
+
"EnterPlanMode",
|
|
9
|
+
"ExitPlanMode",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const RUNTIME_USER_INPUT_TOOLS = new Set(["AskUserQuestion", "ExitPlanMode"]);
|
|
13
|
+
|
|
14
|
+
const HEADLESS_AUTO_ALLOW_TOOLS = new Set(["EnterPlanMode"]);
|
|
15
|
+
|
|
16
|
+
export function isInteractiveApprovalTool(toolName: string): boolean {
|
|
17
|
+
return INTERACTIVE_APPROVAL_TOOLS.has(toolName);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function requiresRuntimeUserInput(toolName: string): boolean {
|
|
21
|
+
return RUNTIME_USER_INPUT_TOOLS.has(toolName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isHeadlessAutoAllowTool(toolName: string): boolean {
|
|
25
|
+
return HEADLESS_AUTO_ALLOW_TOOLS.has(toolName);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizePermissionSuggestions(
|
|
29
|
+
value: unknown,
|
|
30
|
+
): CanUseToolPermissionSuggestion[] | undefined {
|
|
31
|
+
if (!Array.isArray(value)) return undefined;
|
|
32
|
+
const suggestions: CanUseToolPermissionSuggestion[] = [];
|
|
33
|
+
for (const entry of value) {
|
|
34
|
+
if (!entry || typeof entry !== "object") continue;
|
|
35
|
+
const record = entry as Record<string, unknown>;
|
|
36
|
+
if (typeof record.id === "string" && typeof record.text === "string") {
|
|
37
|
+
suggestions.push({ id: record.id, text: record.text });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return suggestions;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the {@link CanUseToolContext} passed to canUseTool callbacks from a raw
|
|
45
|
+
* `can_use_tool` control request body. Fields absent from the wire request are
|
|
46
|
+
* left undefined so callbacks can distinguish "not provided" from empty values.
|
|
47
|
+
*/
|
|
48
|
+
export function buildCanUseToolContext(
|
|
49
|
+
request: Record<string, unknown>,
|
|
50
|
+
requestId?: string,
|
|
51
|
+
): CanUseToolContext {
|
|
52
|
+
const context: CanUseToolContext = {};
|
|
53
|
+
if (typeof requestId === "string") context.requestId = requestId;
|
|
54
|
+
if (typeof request.tool_call_id === "string") context.toolCallId = request.tool_call_id;
|
|
55
|
+
const suggestions = normalizePermissionSuggestions(request.permission_suggestions);
|
|
56
|
+
if (suggestions !== undefined) context.permissionSuggestions = suggestions;
|
|
57
|
+
if (typeof request.blocked_path === "string" || request.blocked_path === null) {
|
|
58
|
+
context.blockedPath = request.blocked_path as string | null;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(request.diffs)) context.diffs = request.diffs as unknown[];
|
|
61
|
+
return context;
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppServerSession,
|
|
3
|
+
type AppServerSessionMode,
|
|
4
|
+
type AppServerSessionOptions,
|
|
5
|
+
} from "./app-server-session.js";
|
|
6
|
+
import { startLocalAppServer } from "./local-app-server.js";
|
|
7
|
+
import type { LettaCodeLocalAppServerOptions } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export function createLocalAppServerSession(
|
|
10
|
+
options: LettaCodeLocalAppServerOptions | undefined,
|
|
11
|
+
mode: AppServerSessionMode,
|
|
12
|
+
beforeConnect?: () => Promise<void>,
|
|
13
|
+
): AppServerSession {
|
|
14
|
+
const appServer = options ?? {};
|
|
15
|
+
const sessionOptions: AppServerSessionOptions = {
|
|
16
|
+
...(appServer.url !== undefined
|
|
17
|
+
? { url: appServer.url }
|
|
18
|
+
: {
|
|
19
|
+
connect: (sessionEnv?: Record<string, string>) =>
|
|
20
|
+
startLocalAppServer({
|
|
21
|
+
listen: appServer.listen,
|
|
22
|
+
backend: appServer.harnessBackend ?? "local",
|
|
23
|
+
startupTimeoutMs: appServer.startupTimeoutMs,
|
|
24
|
+
env: sessionEnv,
|
|
25
|
+
}),
|
|
26
|
+
}),
|
|
27
|
+
...(appServer.WebSocket !== undefined
|
|
28
|
+
? { WebSocket: appServer.WebSocket }
|
|
29
|
+
: {}),
|
|
30
|
+
...(appServer.requestTimeoutMs !== undefined
|
|
31
|
+
? { requestTimeoutMs: appServer.requestTimeoutMs }
|
|
32
|
+
: {}),
|
|
33
|
+
...(appServer.pinGlobalAgent !== undefined
|
|
34
|
+
? { pinGlobalAgent: appServer.pinGlobalAgent }
|
|
35
|
+
: {}),
|
|
36
|
+
...(beforeConnect ? { beforeConnect } : {}),
|
|
37
|
+
};
|
|
38
|
+
return new AppServerSession(sessionOptions, mode);
|
|
39
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { findLettaCli } from "./cli-resolver.js";
|
|
3
|
+
|
|
4
|
+
export interface LocalAppServerHandle {
|
|
5
|
+
url: string;
|
|
6
|
+
close(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StartLocalAppServerOptions {
|
|
10
|
+
listen?: string;
|
|
11
|
+
backend?: string;
|
|
12
|
+
startupTimeoutMs?: number;
|
|
13
|
+
cliPath?: string;
|
|
14
|
+
env?: Record<string, string | undefined>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_LISTEN_URL = "ws://127.0.0.1:0";
|
|
18
|
+
const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
19
|
+
const LISTENING_RE = /^Listening on\s+(ws:\/\/\S+)\s*$/m;
|
|
20
|
+
|
|
21
|
+
function appendLine(buffer: string, chunk: unknown): string {
|
|
22
|
+
return buffer + String(chunk);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tryExtractListeningUrl(output: string): string | null {
|
|
26
|
+
const match = output.match(LISTENING_RE);
|
|
27
|
+
return match?.[1] ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildLocalAppServerArgs(
|
|
31
|
+
cliPath: string,
|
|
32
|
+
options: Pick<StartLocalAppServerOptions, "backend" | "listen"> = {},
|
|
33
|
+
): string[] {
|
|
34
|
+
return [
|
|
35
|
+
cliPath,
|
|
36
|
+
...(options.backend !== undefined ? ["--backend", options.backend] : []),
|
|
37
|
+
"app-server",
|
|
38
|
+
"--listen",
|
|
39
|
+
options.listen ?? DEFAULT_LISTEN_URL,
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function terminateProcess(child: ChildProcess): void {
|
|
44
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
45
|
+
child.kill("SIGTERM");
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
48
|
+
child.kill("SIGKILL");
|
|
49
|
+
}
|
|
50
|
+
}, 1_000).unref?.();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Spawn an SDK-owned Letta Code app-server on an ephemeral loopback port.
|
|
55
|
+
*/
|
|
56
|
+
export function startLocalAppServer(
|
|
57
|
+
options: StartLocalAppServerOptions = {},
|
|
58
|
+
): Promise<LocalAppServerHandle> {
|
|
59
|
+
const cliPath = options.cliPath ?? findLettaCli();
|
|
60
|
+
const args = buildLocalAppServerArgs(cliPath, options);
|
|
61
|
+
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const child = spawn(process.execPath, args, {
|
|
65
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
66
|
+
env: { ...process.env, ...(options.env ?? {}) },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let settled = false;
|
|
70
|
+
let output = "";
|
|
71
|
+
|
|
72
|
+
const cleanup = () => {
|
|
73
|
+
child.stdout?.off("data", onStdout);
|
|
74
|
+
child.stderr?.off("data", onStderr);
|
|
75
|
+
child.off("error", onError);
|
|
76
|
+
child.off("exit", onExit);
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const fail = (error: Error) => {
|
|
81
|
+
if (settled) return;
|
|
82
|
+
settled = true;
|
|
83
|
+
cleanup();
|
|
84
|
+
terminateProcess(child);
|
|
85
|
+
reject(error);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const succeed = (url: string) => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true;
|
|
91
|
+
cleanup();
|
|
92
|
+
resolve({
|
|
93
|
+
url,
|
|
94
|
+
close: () => terminateProcess(child),
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onOutput = (chunk: unknown) => {
|
|
99
|
+
output = appendLine(output, chunk);
|
|
100
|
+
const url = tryExtractListeningUrl(output);
|
|
101
|
+
if (url) succeed(url);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onStdout = (chunk: unknown) => onOutput(chunk);
|
|
105
|
+
const onStderr = (chunk: unknown) => {
|
|
106
|
+
// Startup failures are printed to stderr by the CLI. Keep stderr in the
|
|
107
|
+
// collected output so timeout/exit errors are actionable.
|
|
108
|
+
output = appendLine(output, chunk);
|
|
109
|
+
};
|
|
110
|
+
const onError = (error: Error) => fail(error);
|
|
111
|
+
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
112
|
+
if (settled) return;
|
|
113
|
+
fail(
|
|
114
|
+
new Error(
|
|
115
|
+
`Local Letta Code app-server exited before listening (code=${code ?? "null"}, signal=${signal ?? "null"}).${
|
|
116
|
+
output ? ` Output:\n${output.trim()}` : ""
|
|
117
|
+
}`,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const timeout = setTimeout(() => {
|
|
123
|
+
fail(
|
|
124
|
+
new Error(
|
|
125
|
+
`Timed out waiting for local Letta Code app-server to start.${
|
|
126
|
+
output ? ` Output:\n${output.trim()}` : ""
|
|
127
|
+
}`,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
}, startupTimeoutMs);
|
|
131
|
+
|
|
132
|
+
child.stdout?.on("data", onStdout);
|
|
133
|
+
child.stderr?.on("data", onStderr);
|
|
134
|
+
child.once("error", onError);
|
|
135
|
+
child.once("exit", onExit);
|
|
136
|
+
});
|
|
137
|
+
}
|