@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/transport.ts
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubprocessTransport
|
|
3
|
+
*
|
|
4
|
+
* Spawns the Letta Code CLI and communicates via stdin/stdout JSON streams.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
8
|
+
import { createInterface, type Interface } from "node:readline";
|
|
9
|
+
import { normalizePermissionMode } from "./remote-client-session-core.js";
|
|
10
|
+
import type { InternalSessionOptions, WireMessage } from "./types.js";
|
|
11
|
+
|
|
12
|
+
// All logging gated behind DEBUG_SDK env var
|
|
13
|
+
function sdkLog(tag: string, ...args: unknown[]) {
|
|
14
|
+
if (process.env.DEBUG_SDK) console.error(`[SDK-Transport] [${tag}]`, ...args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SDK_AGENT_ORIGIN_TAG = "origin:letta-code";
|
|
18
|
+
|
|
19
|
+
function shouldApplyNewAgentDefaults(options: InternalSessionOptions): boolean {
|
|
20
|
+
return options.createOnly === true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function includeSdkAgentOriginTag(tags: string[] | undefined): string[] {
|
|
24
|
+
const normalizedTags: string[] = [];
|
|
25
|
+
let hasOriginTag = false;
|
|
26
|
+
|
|
27
|
+
for (const tag of tags ?? []) {
|
|
28
|
+
if (tag === SDK_AGENT_ORIGIN_TAG) {
|
|
29
|
+
if (hasOriginTag) continue;
|
|
30
|
+
hasOriginTag = true;
|
|
31
|
+
}
|
|
32
|
+
normalizedTags.push(tag);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!hasOriginTag) {
|
|
36
|
+
normalizedTags.push(SDK_AGENT_ORIGIN_TAG);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return normalizedTags;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the CLI argument array for a given set of session options.
|
|
44
|
+
*
|
|
45
|
+
* Exported as a pure function for testing — this IS the real production code
|
|
46
|
+
* path. SubprocessTransport.buildArgs() delegates here.
|
|
47
|
+
*/
|
|
48
|
+
export function buildCliArgs(options: InternalSessionOptions): string[] {
|
|
49
|
+
const args: string[] = [
|
|
50
|
+
"--output-format",
|
|
51
|
+
"stream-json",
|
|
52
|
+
"--input-format",
|
|
53
|
+
"stream-json",
|
|
54
|
+
];
|
|
55
|
+
const applyNewAgentDefaults = shouldApplyNewAgentDefaults(options);
|
|
56
|
+
|
|
57
|
+
// Conversation and agent handling
|
|
58
|
+
if (options.conversationId) {
|
|
59
|
+
args.push("--conversation", options.conversationId);
|
|
60
|
+
} else if (options.agentId) {
|
|
61
|
+
args.push("--agent", options.agentId);
|
|
62
|
+
if (options.newConversation) {
|
|
63
|
+
args.push("--new");
|
|
64
|
+
} else if (options.defaultConversation) {
|
|
65
|
+
args.push("--conversation", "default");
|
|
66
|
+
}
|
|
67
|
+
} else if (options.createOnly) {
|
|
68
|
+
args.push("--new-agent");
|
|
69
|
+
} else if (options.newConversation) {
|
|
70
|
+
args.push("--new");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Model
|
|
74
|
+
if (options.model) {
|
|
75
|
+
args.push("-m", options.model);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Partial message streaming
|
|
79
|
+
if (options.includePartialMessages) {
|
|
80
|
+
args.push("--include-partial-messages");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Embedding model
|
|
84
|
+
if (options.embedding) {
|
|
85
|
+
args.push("--embedding", options.embedding);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// System prompt configuration
|
|
89
|
+
if (options.systemPrompt !== undefined) {
|
|
90
|
+
if (typeof options.systemPrompt === "string") {
|
|
91
|
+
const validPresets = [
|
|
92
|
+
"default",
|
|
93
|
+
"letta-claude",
|
|
94
|
+
"letta-codex",
|
|
95
|
+
"letta-gemini",
|
|
96
|
+
"claude",
|
|
97
|
+
"codex",
|
|
98
|
+
"gemini",
|
|
99
|
+
];
|
|
100
|
+
if (validPresets.includes(options.systemPrompt)) {
|
|
101
|
+
args.push("--system", options.systemPrompt);
|
|
102
|
+
} else {
|
|
103
|
+
args.push("--system-custom", options.systemPrompt);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
args.push("--system", options.systemPrompt.preset);
|
|
107
|
+
if (options.systemPrompt.append) {
|
|
108
|
+
args.push("--system-append", options.systemPrompt.append);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Memory blocks (only for new agents)
|
|
114
|
+
if (options.memory !== undefined && !options.agentId) {
|
|
115
|
+
if (options.memory.length === 0) {
|
|
116
|
+
args.push("--init-blocks", "");
|
|
117
|
+
} else {
|
|
118
|
+
const presetNames: string[] = [];
|
|
119
|
+
const memoryBlocksJson: Array<
|
|
120
|
+
| { label: string; value: string }
|
|
121
|
+
| { blockId: string }
|
|
122
|
+
> = [];
|
|
123
|
+
|
|
124
|
+
for (const item of options.memory) {
|
|
125
|
+
if (typeof item === "string") {
|
|
126
|
+
presetNames.push(item);
|
|
127
|
+
} else if ("blockId" in item) {
|
|
128
|
+
memoryBlocksJson.push(item as { blockId: string });
|
|
129
|
+
} else {
|
|
130
|
+
memoryBlocksJson.push(item as { label: string; value: string });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (memoryBlocksJson.length > 0) {
|
|
135
|
+
args.push("--memory-blocks", JSON.stringify(memoryBlocksJson));
|
|
136
|
+
if (presetNames.length > 0) {
|
|
137
|
+
console.warn(
|
|
138
|
+
"[letta-agent-sdk] Using custom memory blocks. " +
|
|
139
|
+
`Preset blocks are ignored when custom blocks are provided: ${presetNames.join(", ")}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
} else if (presetNames.length > 0) {
|
|
143
|
+
args.push("--init-blocks", presetNames.join(","));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Convenience props for block values (only for new agents)
|
|
149
|
+
if (!options.agentId) {
|
|
150
|
+
if (options.persona !== undefined) {
|
|
151
|
+
args.push("--block-value", `persona=${options.persona}`);
|
|
152
|
+
}
|
|
153
|
+
if (options.human !== undefined) {
|
|
154
|
+
args.push("--block-value", `human=${options.human}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const mode = normalizePermissionMode(options.permissionMode);
|
|
159
|
+
if (mode !== undefined) {
|
|
160
|
+
args.push("--permission-mode", mode);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Allowed / disallowed tools. Interactive user-input tools need no handling
|
|
164
|
+
// here: the CLI's headless path already excludes them from the toolset.
|
|
165
|
+
if (options.allowedTools) {
|
|
166
|
+
args.push("--allowedTools", options.allowedTools.join(","));
|
|
167
|
+
}
|
|
168
|
+
if (options.disallowedTools) {
|
|
169
|
+
args.push("--disallowedTools", options.disallowedTools.join(","));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Tags
|
|
173
|
+
const tags = applyNewAgentDefaults
|
|
174
|
+
? includeSdkAgentOriginTag(options.tags)
|
|
175
|
+
: options.tags;
|
|
176
|
+
if (tags && tags.length > 0) {
|
|
177
|
+
args.push("--tags", tags.join(","));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// SDK-created agents always use the git-backed memory filesystem.
|
|
181
|
+
if (applyNewAgentDefaults) {
|
|
182
|
+
args.push("--memfs");
|
|
183
|
+
// When omitted, the CLI applies its created-agent defaults. An explicit
|
|
184
|
+
// list is forwarded; "none" is the CLI's sentinel for an empty list.
|
|
185
|
+
if (options.baseTools !== undefined) {
|
|
186
|
+
args.push(
|
|
187
|
+
"--base-tools",
|
|
188
|
+
options.baseTools.length > 0 ? options.baseTools.join(",") : "none",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Skills sources
|
|
194
|
+
if (options.skillSources !== undefined) {
|
|
195
|
+
const sources = [...new Set(options.skillSources)];
|
|
196
|
+
if (sources.length === 0) {
|
|
197
|
+
args.push("--no-skills");
|
|
198
|
+
} else {
|
|
199
|
+
args.push("--skill-sources", sources.join(","));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Session context reminder toggle
|
|
204
|
+
if (options.systemInfoReminder === false) {
|
|
205
|
+
args.push("--no-system-info-reminder");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Dreaming settings (forwarded to the CLI's underlying reflection flags)
|
|
209
|
+
if (options.dreaming?.trigger !== undefined) {
|
|
210
|
+
args.push("--reflection-trigger", options.dreaming.trigger);
|
|
211
|
+
}
|
|
212
|
+
if (options.dreaming?.behavior !== undefined) {
|
|
213
|
+
args.push("--reflection-behavior", options.dreaming.behavior);
|
|
214
|
+
}
|
|
215
|
+
if (options.dreaming?.stepCount !== undefined) {
|
|
216
|
+
args.push("--reflection-step-count", String(options.dreaming.stepCount));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return args;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export class SubprocessTransport {
|
|
223
|
+
private process: ChildProcess | null = null;
|
|
224
|
+
private stdout: Interface | null = null;
|
|
225
|
+
private messageQueue: WireMessage[] = [];
|
|
226
|
+
private messageResolvers: Array<(msg: WireMessage | null) => void> = [];
|
|
227
|
+
private closed = false;
|
|
228
|
+
private agentId?: string;
|
|
229
|
+
private wireMessageCount = 0;
|
|
230
|
+
private lastMessageAt = 0;
|
|
231
|
+
private stderrLines: string[] = [];
|
|
232
|
+
|
|
233
|
+
constructor(
|
|
234
|
+
private options: InternalSessionOptions = {}
|
|
235
|
+
) {}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Start the CLI subprocess
|
|
239
|
+
*/
|
|
240
|
+
async connect(): Promise<void> {
|
|
241
|
+
const args = this.buildArgs();
|
|
242
|
+
|
|
243
|
+
// Find the CLI - use the installed letta-code package
|
|
244
|
+
const cliPath = await this.findCli();
|
|
245
|
+
sdkLog("connect", `CLI: ${cliPath}`);
|
|
246
|
+
sdkLog("connect", `args: ${args.join(" ")}`);
|
|
247
|
+
sdkLog("connect", `cwd: ${this.options.cwd || process.cwd()}`);
|
|
248
|
+
sdkLog("connect", `permissionMode: ${this.options.permissionMode || "default"}`);
|
|
249
|
+
|
|
250
|
+
this.process = spawn("node", [cliPath, ...args], {
|
|
251
|
+
cwd: this.options.cwd || process.cwd(),
|
|
252
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
253
|
+
env: { ...process.env },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const pid = this.process.pid;
|
|
257
|
+
sdkLog("connect", `CLI process spawned, pid=${pid}`);
|
|
258
|
+
|
|
259
|
+
if (!this.process.stdout || !this.process.stdin) {
|
|
260
|
+
throw new Error("Failed to create subprocess pipes");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Set up stdout reading
|
|
264
|
+
this.stdout = createInterface({
|
|
265
|
+
input: this.process.stdout,
|
|
266
|
+
crlfDelay: Infinity,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.stdout.on("line", (line) => {
|
|
270
|
+
if (!line.trim()) return;
|
|
271
|
+
try {
|
|
272
|
+
const msg = JSON.parse(line) as WireMessage;
|
|
273
|
+
this.handleMessage(msg);
|
|
274
|
+
} catch {
|
|
275
|
+
// Non-JSON line from CLI stdout - could be important debug info
|
|
276
|
+
sdkLog("stdout", `[non-JSON] ${line.slice(0, 500)}`);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Log stderr for debugging (CLI errors, auth failures, etc.)
|
|
281
|
+
// Also buffer lines so session.initialize() can include them in errors.
|
|
282
|
+
if (this.process.stderr) {
|
|
283
|
+
this.process.stderr.on("data", (data: Buffer) => {
|
|
284
|
+
const msg = data.toString().trim();
|
|
285
|
+
if (msg) {
|
|
286
|
+
console.error("[letta-agent-sdk] CLI stderr:", msg);
|
|
287
|
+
this.stderrLines.push(msg);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Handle process exit
|
|
293
|
+
//
|
|
294
|
+
// BUG FIX: When the CLI subprocess exits while read() has a pending
|
|
295
|
+
// resolver waiting for the next message, that resolver would never fire.
|
|
296
|
+
// The messages() async generator would be stuck in `await this.read()`
|
|
297
|
+
// forever, causing session.stream() to hang, which deadlocks the
|
|
298
|
+
// caller's processing mutex. Resolving pending readers with null on
|
|
299
|
+
// process exit lets messages() break out of its loop cleanly.
|
|
300
|
+
this.process.on("close", (code, signal) => {
|
|
301
|
+
if (code !== 0 && code !== null) {
|
|
302
|
+
console.error(`[letta-agent-sdk] CLI process exited with code ${code}`);
|
|
303
|
+
}
|
|
304
|
+
sdkLog("close", `CLI process exited: pid=${pid} code=${code} signal=${signal} wireMessages=${this.wireMessageCount} msSinceLastMsg=${this.lastMessageAt ? Date.now() - this.lastMessageAt : 0} pendingResolvers=${this.messageResolvers.length} queueLen=${this.messageQueue.length}`);
|
|
305
|
+
this.closed = true;
|
|
306
|
+
// Flush pending readers so they don't hang forever (see comment above)
|
|
307
|
+
for (const resolve of this.messageResolvers) {
|
|
308
|
+
resolve(null);
|
|
309
|
+
}
|
|
310
|
+
this.messageResolvers = [];
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
this.process.on("error", (err) => {
|
|
314
|
+
console.error("[letta-agent-sdk] CLI process error:", err);
|
|
315
|
+
this.closed = true;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Send a message to the CLI via stdin
|
|
321
|
+
*/
|
|
322
|
+
async write(data: object): Promise<void> {
|
|
323
|
+
if (!this.process?.stdin || this.closed) {
|
|
324
|
+
const err = new Error(`Transport not connected (closed=${this.closed}, pid=${this.process?.pid}, stdin=${!!this.process?.stdin})`);
|
|
325
|
+
sdkLog("write", err.message);
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
const payload = data as Record<string, unknown>;
|
|
329
|
+
sdkLog("write", `type=${payload.type} subtype=${(payload.request as Record<string, unknown>)?.subtype || (payload.response as Record<string, unknown>)?.subtype || "N/A"}`);
|
|
330
|
+
this.process.stdin.write(JSON.stringify(data) + "\n");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Read the next message from the CLI
|
|
335
|
+
*/
|
|
336
|
+
async read(): Promise<WireMessage | null> {
|
|
337
|
+
// Return queued message if available
|
|
338
|
+
if (this.messageQueue.length > 0) {
|
|
339
|
+
return this.messageQueue.shift()!;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// If closed, no more messages
|
|
343
|
+
if (this.closed) {
|
|
344
|
+
sdkLog("read", `returning null (closed), total wireMessages=${this.wireMessageCount}`);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Wait for next message
|
|
349
|
+
sdkLog("read", `waiting for next message (resolvers=${this.messageResolvers.length + 1}, queue=${this.messageQueue.length})`);
|
|
350
|
+
return new Promise((resolve) => {
|
|
351
|
+
this.messageResolvers.push(resolve);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Async iterator for messages
|
|
357
|
+
*/
|
|
358
|
+
async *messages(): AsyncGenerator<WireMessage> {
|
|
359
|
+
while (true) {
|
|
360
|
+
const msg = await this.read();
|
|
361
|
+
if (msg === null) {
|
|
362
|
+
sdkLog("messages", `iterator ending (closed=${this.closed}, wireMessages=${this.wireMessageCount})`);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
yield msg;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Close the transport
|
|
371
|
+
*/
|
|
372
|
+
close(): void {
|
|
373
|
+
sdkLog("close", `explicit close called (wireMessages=${this.wireMessageCount}, pendingResolvers=${this.messageResolvers.length}, pid=${this.process?.pid})`);
|
|
374
|
+
if (this.process) {
|
|
375
|
+
this.process.stdin?.end();
|
|
376
|
+
this.process.kill();
|
|
377
|
+
this.process = null;
|
|
378
|
+
}
|
|
379
|
+
this.closed = true;
|
|
380
|
+
|
|
381
|
+
// Resolve any pending readers with null
|
|
382
|
+
for (const resolve of this.messageResolvers) {
|
|
383
|
+
resolve(null);
|
|
384
|
+
}
|
|
385
|
+
this.messageResolvers = [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
get isClosed(): boolean {
|
|
389
|
+
return this.closed;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Return buffered stderr output from the CLI subprocess. */
|
|
393
|
+
getStderr(): string {
|
|
394
|
+
return this.stderrLines.join("\n");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private handleMessage(msg: WireMessage): void {
|
|
398
|
+
this.wireMessageCount++;
|
|
399
|
+
this.lastMessageAt = Date.now();
|
|
400
|
+
|
|
401
|
+
// Compact log of every wire message for traceability
|
|
402
|
+
const wirePayload = msg as unknown as Record<string, unknown>;
|
|
403
|
+
const msgType = wirePayload.message_type || wirePayload.subtype || "";
|
|
404
|
+
sdkLog("wire", `#${this.wireMessageCount} type=${msg.type} ${msgType ? `msg_type=${msgType}` : ""} resolvers=${this.messageResolvers.length} queue=${this.messageQueue.length}`);
|
|
405
|
+
|
|
406
|
+
// Always log critical message types (result, errors, approval)
|
|
407
|
+
if (msg.type === "result") {
|
|
408
|
+
const result = wirePayload as unknown as { subtype?: string; result?: string; duration_ms?: number; stop_reason?: string };
|
|
409
|
+
sdkLog("wire", `RESULT: subtype=${result.subtype} stop_reason=${result.stop_reason || "N/A"} duration=${result.duration_ms}ms resultLen=${result.result?.length || 0}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Track agent_id from init message
|
|
413
|
+
if (msg.type === "system" && "subtype" in msg && msg.subtype === "init") {
|
|
414
|
+
this.agentId = (msg as unknown as { agent_id: string }).agent_id;
|
|
415
|
+
sdkLog("wire", `INIT: agent_id=${this.agentId}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Log control requests (approval flow)
|
|
419
|
+
if (msg.type === "control_request") {
|
|
420
|
+
const req = wirePayload as unknown as { request_id?: string; request?: { subtype?: string; tool_name?: string } };
|
|
421
|
+
sdkLog("wire", `CONTROL_REQUEST: id=${req.request_id} subtype=${req.request?.subtype} tool=${req.request?.tool_name || "N/A"}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If someone is waiting for a message, give it to them
|
|
425
|
+
if (this.messageResolvers.length > 0) {
|
|
426
|
+
const resolve = this.messageResolvers.shift()!;
|
|
427
|
+
resolve(msg);
|
|
428
|
+
} else {
|
|
429
|
+
// Otherwise queue it
|
|
430
|
+
this.messageQueue.push(msg);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private buildArgs(): string[] {
|
|
435
|
+
return buildCliArgs(this.options);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
private async findCli(): Promise<string> {
|
|
441
|
+
// Try multiple resolution strategies
|
|
442
|
+
const { existsSync } = await import("node:fs");
|
|
443
|
+
const { dirname, join } = await import("node:path");
|
|
444
|
+
const { fileURLToPath } = await import("node:url");
|
|
445
|
+
|
|
446
|
+
// Strategy 1: Check LETTA_CLI_PATH env var
|
|
447
|
+
if (process.env.LETTA_CLI_PATH && existsSync(process.env.LETTA_CLI_PATH)) {
|
|
448
|
+
return process.env.LETTA_CLI_PATH;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Strategy 2: Try to resolve from node_modules
|
|
452
|
+
// Note: resolve the package main export (not /letta.js subpath) because
|
|
453
|
+
// the package.json "exports" field doesn't expose the subpath directly.
|
|
454
|
+
try {
|
|
455
|
+
const { createRequire } = await import("node:module");
|
|
456
|
+
const require = createRequire(import.meta.url);
|
|
457
|
+
const resolved = require.resolve("@letta-ai/letta-code");
|
|
458
|
+
if (existsSync(resolved)) {
|
|
459
|
+
return resolved;
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
// Continue to next strategy
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Strategy 3: Check relative to this file (for local file: deps)
|
|
466
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
467
|
+
const __dirname = dirname(__filename);
|
|
468
|
+
const localPaths = [
|
|
469
|
+
join(__dirname, "../../@letta-ai/letta-code/letta.js"),
|
|
470
|
+
join(__dirname, "../../../letta-code-prod/letta.js"),
|
|
471
|
+
join(__dirname, "../../../letta-code/letta.js"),
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
for (const p of localPaths) {
|
|
475
|
+
if (existsSync(p)) {
|
|
476
|
+
return p;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
throw new Error(
|
|
481
|
+
"Letta Code CLI not found. Set LETTA_CLI_PATH or install @letta-ai/letta-code."
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|