@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,505 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SDKLoopStatusMessage,
|
|
3
|
+
SDKMessage,
|
|
4
|
+
SDKQueueUpdateMessage,
|
|
5
|
+
SDKResultMessage,
|
|
6
|
+
SDKStreamEventPayload,
|
|
7
|
+
SessionDeviceStatus,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import {
|
|
10
|
+
FAILURE_STOP_REASONS,
|
|
11
|
+
deviceStatusRecord,
|
|
12
|
+
extractTextFromContent,
|
|
13
|
+
firstToolCall,
|
|
14
|
+
firstToolReturn,
|
|
15
|
+
isApprovalConflictSignal,
|
|
16
|
+
loopStatusRunIds,
|
|
17
|
+
loopStatusValue,
|
|
18
|
+
queueItems,
|
|
19
|
+
sameRuntime,
|
|
20
|
+
streamDeltaMessageType,
|
|
21
|
+
streamDeltaRecord,
|
|
22
|
+
streamDeltaRunId,
|
|
23
|
+
streamDeltaStopReason,
|
|
24
|
+
toSdkErrorCode,
|
|
25
|
+
toSessionDeviceStatus,
|
|
26
|
+
toolInputFromArguments,
|
|
27
|
+
type ProtocolMessage,
|
|
28
|
+
type RuntimeScope,
|
|
29
|
+
type RuntimeTurnResult,
|
|
30
|
+
type TurnTracker,
|
|
31
|
+
} from "./remote-session-protocol.js";
|
|
32
|
+
|
|
33
|
+
type RemoteTurnCoordinatorConfig = {
|
|
34
|
+
label: string;
|
|
35
|
+
requestTimeoutMs?: number;
|
|
36
|
+
onDeviceStatus(status: SessionDeviceStatus): void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Owns the portable session's turn correlation and stream queue.
|
|
41
|
+
*
|
|
42
|
+
* Transport setup and public SDK methods stay in RemoteClientSessionCore.
|
|
43
|
+
* This class only translates protocol events into SDK messages and correlates
|
|
44
|
+
* those events with active or queued turns.
|
|
45
|
+
*/
|
|
46
|
+
export class RemoteTurnCoordinator {
|
|
47
|
+
private readonly label: string;
|
|
48
|
+
private readonly requestTimeoutMs: number | undefined;
|
|
49
|
+
private readonly onDeviceStatus: (status: SessionDeviceStatus) => void;
|
|
50
|
+
private streamQueue: SDKMessage[] = [];
|
|
51
|
+
private streamResolvers: Array<(message: SDKMessage | null) => void> = [];
|
|
52
|
+
private activeTurn: TurnTracker | null = null;
|
|
53
|
+
private pendingTurns: TurnTracker[] = [];
|
|
54
|
+
private nextTurnId = 0;
|
|
55
|
+
private messageCounter = 0;
|
|
56
|
+
private clientMessageCounter = 0;
|
|
57
|
+
private closed = false;
|
|
58
|
+
private _activeTurnStartedAt = 0;
|
|
59
|
+
|
|
60
|
+
constructor(config: RemoteTurnCoordinatorConfig) {
|
|
61
|
+
this.label = config.label;
|
|
62
|
+
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
63
|
+
this.onDeviceStatus = config.onDeviceStatus;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get activeTurnStartedAt(): number {
|
|
67
|
+
return this._activeTurnStartedAt;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hasInFlightTurn(): boolean {
|
|
71
|
+
return this.activeTurn !== null || this.pendingTurns.length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
trackSentTurn(runtime: RuntimeScope): TurnTracker {
|
|
75
|
+
const turn: TurnTracker = {
|
|
76
|
+
id: ++this.nextTurnId,
|
|
77
|
+
runtime,
|
|
78
|
+
clientMessageId: `sdk-message-${Date.now()}-${++this.clientMessageCounter}`,
|
|
79
|
+
queuedAt: Date.now(),
|
|
80
|
+
startedAt: 0,
|
|
81
|
+
assistantText: "",
|
|
82
|
+
runIds: new Set<string>(),
|
|
83
|
+
observedTurnEvidence: false,
|
|
84
|
+
observedRequiresApprovalStop: false,
|
|
85
|
+
abortRequested: false,
|
|
86
|
+
timeout: null,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (this.activeTurn) {
|
|
90
|
+
this.pendingTurns.push(turn);
|
|
91
|
+
} else {
|
|
92
|
+
this.activateTurn(turn);
|
|
93
|
+
}
|
|
94
|
+
return turn;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
removeTrackedTurn(turn: TurnTracker): void {
|
|
98
|
+
if (turn.timeout) clearTimeout(turn.timeout);
|
|
99
|
+
if (this.activeTurn === turn) {
|
|
100
|
+
this.activeTurn = null;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const index = this.pendingTurns.indexOf(turn);
|
|
104
|
+
if (index !== -1) this.pendingTurns.splice(index, 1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
markAbortRequested(): void {
|
|
108
|
+
if (this.activeTurn) this.activeTurn.abortRequested = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
handleProtocolMessage(message: ProtocolMessage, runtime: RuntimeScope): void {
|
|
112
|
+
if (!sameRuntime(message, runtime)) return;
|
|
113
|
+
|
|
114
|
+
const statusRecord = deviceStatusRecord(message);
|
|
115
|
+
if (statusRecord) {
|
|
116
|
+
const status = toSessionDeviceStatus(statusRecord);
|
|
117
|
+
if (status) this.onDeviceStatus(status);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (message.type === "update_queue") {
|
|
122
|
+
const sdkMessage: SDKQueueUpdateMessage = {
|
|
123
|
+
type: "queue_update",
|
|
124
|
+
queue: queueItems(message),
|
|
125
|
+
};
|
|
126
|
+
this.enqueue(sdkMessage);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (message.type === "update_loop_status") {
|
|
131
|
+
this.handleLoopStatusMessage(message);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const delta = streamDeltaRecord(message);
|
|
136
|
+
if (!delta) return;
|
|
137
|
+
const active = this.activateNextTurnFromProtocol();
|
|
138
|
+
if (active) {
|
|
139
|
+
active.observedTurnEvidence = true;
|
|
140
|
+
const runId = streamDeltaRunId(delta);
|
|
141
|
+
if (runId) active.runIds.add(runId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const sdkMessage = this.transformStreamDelta(delta);
|
|
145
|
+
if (sdkMessage) this.enqueue(sdkMessage);
|
|
146
|
+
this.handleTurnTerminalDelta(delta, sdkMessage);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
nextMessage(): Promise<SDKMessage | null> {
|
|
150
|
+
const next = this.streamQueue.shift();
|
|
151
|
+
if (next) return Promise.resolve(next);
|
|
152
|
+
if (this.closed) return Promise.resolve(null);
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
this.streamResolvers.push(resolve);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
close(): void {
|
|
159
|
+
if (this.closed) return;
|
|
160
|
+
this.closed = true;
|
|
161
|
+
if (this.activeTurn?.timeout) clearTimeout(this.activeTurn.timeout);
|
|
162
|
+
for (const turn of this.pendingTurns) {
|
|
163
|
+
if (turn.timeout) clearTimeout(turn.timeout);
|
|
164
|
+
}
|
|
165
|
+
this.activeTurn = null;
|
|
166
|
+
this.pendingTurns.length = 0;
|
|
167
|
+
this.resolveAll(null);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private activateTurn(turn: TurnTracker): void {
|
|
171
|
+
this.activeTurn = turn;
|
|
172
|
+
turn.startedAt = Date.now();
|
|
173
|
+
this._activeTurnStartedAt = turn.startedAt;
|
|
174
|
+
if (this.requestTimeoutMs !== undefined) {
|
|
175
|
+
turn.timeout = setTimeout(() => {
|
|
176
|
+
this.failTurn(turn, `Timed out waiting for ${this.label} turn`);
|
|
177
|
+
}, this.requestTimeoutMs);
|
|
178
|
+
(turn.timeout as { unref?: () => void }).unref?.();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private activateNextTurnFromProtocol(): TurnTracker | null {
|
|
183
|
+
if (this.activeTurn) return this.activeTurn;
|
|
184
|
+
const next = this.pendingTurns.shift();
|
|
185
|
+
if (!next) return null;
|
|
186
|
+
this.activateTurn(next);
|
|
187
|
+
return next;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private failTurn(turn: TurnTracker, detail: string): void {
|
|
191
|
+
if (this.activeTurn !== turn) return;
|
|
192
|
+
this.enqueue({
|
|
193
|
+
type: "error",
|
|
194
|
+
message: detail,
|
|
195
|
+
errorCode: "error",
|
|
196
|
+
stopReason: "error",
|
|
197
|
+
errorDetail: detail,
|
|
198
|
+
recoverable: false,
|
|
199
|
+
});
|
|
200
|
+
this.completeActiveTurn({
|
|
201
|
+
runtime: turn.runtime,
|
|
202
|
+
stopReason: "error",
|
|
203
|
+
runIds: [...turn.runIds],
|
|
204
|
+
success: false,
|
|
205
|
+
detail,
|
|
206
|
+
errorCode: "error",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private completeActiveTurn(turn: RuntimeTurnResult): void {
|
|
211
|
+
const active = this.activeTurn;
|
|
212
|
+
if (!active) return;
|
|
213
|
+
if (active.timeout) {
|
|
214
|
+
clearTimeout(active.timeout);
|
|
215
|
+
active.timeout = null;
|
|
216
|
+
}
|
|
217
|
+
this.enqueue(this.resultFromTurn(turn, active));
|
|
218
|
+
this.activeTurn = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private handleLoopStatusMessage(message: ProtocolMessage): void {
|
|
222
|
+
const status = loopStatusValue(message);
|
|
223
|
+
if (!status) return;
|
|
224
|
+
const activeRunIds = loopStatusRunIds(message);
|
|
225
|
+
const sdkMessage: SDKLoopStatusMessage = {
|
|
226
|
+
type: "loop_status",
|
|
227
|
+
status,
|
|
228
|
+
activeRunIds,
|
|
229
|
+
};
|
|
230
|
+
this.enqueue(sdkMessage);
|
|
231
|
+
|
|
232
|
+
const active = this.activeTurn;
|
|
233
|
+
if (!active) return;
|
|
234
|
+
for (const runId of activeRunIds) active.runIds.add(runId);
|
|
235
|
+
|
|
236
|
+
const hadTurnEvidence =
|
|
237
|
+
active.observedTurnEvidence || active.observedRequiresApprovalStop;
|
|
238
|
+
if (!hadTurnEvidence) return;
|
|
239
|
+
if (status === "WAITING_ON_APPROVAL") {
|
|
240
|
+
this.completeActiveTurn({
|
|
241
|
+
runtime: active.runtime,
|
|
242
|
+
stopReason: "requires_approval",
|
|
243
|
+
runIds: [...active.runIds],
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (status === "WAITING_ON_INPUT" && active.abortRequested) {
|
|
248
|
+
this.completeActiveTurn({
|
|
249
|
+
runtime: active.runtime,
|
|
250
|
+
stopReason: "interrupted",
|
|
251
|
+
runIds: [...active.runIds],
|
|
252
|
+
success: false,
|
|
253
|
+
detail: "Interrupted",
|
|
254
|
+
errorCode: "interrupted",
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (status === "WAITING_ON_INPUT" && active.observedTurnEvidence) {
|
|
259
|
+
this.completeActiveTurn({
|
|
260
|
+
runtime: active.runtime,
|
|
261
|
+
stopReason: null,
|
|
262
|
+
runIds: [...active.runIds],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private handleTurnTerminalDelta(
|
|
268
|
+
delta: Record<string, unknown>,
|
|
269
|
+
sdkMessage: SDKMessage | null,
|
|
270
|
+
): void {
|
|
271
|
+
const active = this.activeTurn;
|
|
272
|
+
if (!active) return;
|
|
273
|
+
const messageType = streamDeltaMessageType(delta);
|
|
274
|
+
if (messageType === "stop_reason") {
|
|
275
|
+
const stopReason = streamDeltaStopReason(delta) ?? null;
|
|
276
|
+
if (stopReason === "requires_approval") {
|
|
277
|
+
active.observedRequiresApprovalStop = true;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.completeActiveTurn({
|
|
281
|
+
runtime: active.runtime,
|
|
282
|
+
stopReason,
|
|
283
|
+
runIds: [...active.runIds],
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (sdkMessage?.type === "error") {
|
|
289
|
+
this.completeActiveTurn({
|
|
290
|
+
runtime: active.runtime,
|
|
291
|
+
stopReason: sdkMessage.stopReason,
|
|
292
|
+
runIds: [...active.runIds],
|
|
293
|
+
success: false,
|
|
294
|
+
detail: sdkMessage.errorDetail ?? sdkMessage.message,
|
|
295
|
+
errorCode: sdkMessage.errorCode,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private transformStreamDelta(
|
|
301
|
+
delta: Record<string, unknown>,
|
|
302
|
+
): SDKMessage | null {
|
|
303
|
+
const messageType =
|
|
304
|
+
typeof delta.message_type === "string" ? delta.message_type : undefined;
|
|
305
|
+
const runId = typeof delta.run_id === "string" ? delta.run_id : undefined;
|
|
306
|
+
const uuid =
|
|
307
|
+
typeof delta.id === "string"
|
|
308
|
+
? delta.id
|
|
309
|
+
: `${this.label}-${++this.messageCounter}`;
|
|
310
|
+
|
|
311
|
+
if (messageType === "assistant_message") {
|
|
312
|
+
const content = extractTextFromContent(delta.content);
|
|
313
|
+
if (!content) return null;
|
|
314
|
+
if (this.activeTurn) this.activeTurn.assistantText += content;
|
|
315
|
+
return { type: "assistant", content, uuid, runId };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (messageType === "reasoning_message") {
|
|
319
|
+
const content =
|
|
320
|
+
typeof delta.reasoning === "string"
|
|
321
|
+
? delta.reasoning
|
|
322
|
+
: extractTextFromContent(delta.content);
|
|
323
|
+
if (!content) return null;
|
|
324
|
+
return { type: "reasoning", content, uuid, runId };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (
|
|
328
|
+
messageType === "tool_call_message" ||
|
|
329
|
+
messageType === "approval_request_message"
|
|
330
|
+
) {
|
|
331
|
+
const toolCall = firstToolCall(delta);
|
|
332
|
+
if (!toolCall) return null;
|
|
333
|
+
const fn =
|
|
334
|
+
toolCall.function && typeof toolCall.function === "object"
|
|
335
|
+
? (toolCall.function as Record<string, unknown>)
|
|
336
|
+
: undefined;
|
|
337
|
+
const toolCallId =
|
|
338
|
+
(typeof toolCall.tool_call_id === "string"
|
|
339
|
+
? toolCall.tool_call_id
|
|
340
|
+
: undefined) ??
|
|
341
|
+
(typeof toolCall.id === "string" ? toolCall.id : undefined);
|
|
342
|
+
if (!toolCallId) {
|
|
343
|
+
const detail = `Missing tool_call_id in ${messageType} (uuid=${uuid})`;
|
|
344
|
+
return {
|
|
345
|
+
type: "error",
|
|
346
|
+
message: detail,
|
|
347
|
+
errorCode: "protocol_error",
|
|
348
|
+
stopReason: "protocol_error",
|
|
349
|
+
runId,
|
|
350
|
+
recoverable: false,
|
|
351
|
+
errorDetail: detail,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const toolName =
|
|
355
|
+
(typeof toolCall.name === "string" ? toolCall.name : undefined) ??
|
|
356
|
+
(typeof fn?.name === "string" ? fn.name : undefined) ??
|
|
357
|
+
"?";
|
|
358
|
+
const { input, raw } = toolInputFromArguments(
|
|
359
|
+
toolCall.arguments ?? fn?.arguments,
|
|
360
|
+
);
|
|
361
|
+
return {
|
|
362
|
+
type: "tool_call",
|
|
363
|
+
toolCallId,
|
|
364
|
+
toolName,
|
|
365
|
+
toolInput: input,
|
|
366
|
+
rawArguments: raw,
|
|
367
|
+
uuid,
|
|
368
|
+
runId,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (messageType === "tool_return_message") {
|
|
373
|
+
const toolReturn = firstToolReturn(delta) ?? delta;
|
|
374
|
+
const toolCallId =
|
|
375
|
+
(typeof delta.tool_call_id === "string"
|
|
376
|
+
? delta.tool_call_id
|
|
377
|
+
: undefined) ??
|
|
378
|
+
(typeof toolReturn.tool_call_id === "string"
|
|
379
|
+
? toolReturn.tool_call_id
|
|
380
|
+
: undefined);
|
|
381
|
+
if (!toolCallId) return null;
|
|
382
|
+
const content =
|
|
383
|
+
extractTextFromContent(
|
|
384
|
+
delta.tool_return ?? toolReturn.tool_return ?? toolReturn.content,
|
|
385
|
+
) ?? "";
|
|
386
|
+
const status =
|
|
387
|
+
typeof delta.status === "string" ? delta.status : toolReturn.status;
|
|
388
|
+
return {
|
|
389
|
+
type: "tool_result",
|
|
390
|
+
toolCallId,
|
|
391
|
+
content,
|
|
392
|
+
isError: status === "error",
|
|
393
|
+
uuid,
|
|
394
|
+
runId,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (messageType === "error_message" || messageType === "loop_error") {
|
|
399
|
+
const detail =
|
|
400
|
+
(typeof delta.detail === "string" ? delta.detail : undefined) ??
|
|
401
|
+
(typeof delta.message === "string" ? delta.message : undefined) ??
|
|
402
|
+
`${this.label} turn failed`;
|
|
403
|
+
const stopReason =
|
|
404
|
+
(typeof delta.stop_reason === "string"
|
|
405
|
+
? delta.stop_reason
|
|
406
|
+
: undefined) ??
|
|
407
|
+
(typeof delta.error_type === "string"
|
|
408
|
+
? delta.error_type
|
|
409
|
+
: undefined) ??
|
|
410
|
+
"error";
|
|
411
|
+
const approvalConflict = isApprovalConflictSignal({
|
|
412
|
+
detail,
|
|
413
|
+
message:
|
|
414
|
+
typeof delta.message === "string" ? delta.message : undefined,
|
|
415
|
+
stopReason,
|
|
416
|
+
});
|
|
417
|
+
return {
|
|
418
|
+
type: "error",
|
|
419
|
+
message: detail,
|
|
420
|
+
errorCode: approvalConflict
|
|
421
|
+
? "approval_conflict"
|
|
422
|
+
: toSdkErrorCode(stopReason),
|
|
423
|
+
approvalConflict: approvalConflict || undefined,
|
|
424
|
+
recoverable: approvalConflict ? true : false,
|
|
425
|
+
errorDetail: detail,
|
|
426
|
+
stopReason,
|
|
427
|
+
runId,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (messageType === "retry") {
|
|
432
|
+
return {
|
|
433
|
+
type: "retry",
|
|
434
|
+
reason: typeof delta.reason === "string" ? delta.reason : "error",
|
|
435
|
+
attempt: typeof delta.attempt === "number" ? delta.attempt : 0,
|
|
436
|
+
maxAttempts:
|
|
437
|
+
typeof delta.max_attempts === "number" ? delta.max_attempts : 0,
|
|
438
|
+
delayMs: typeof delta.delay_ms === "number" ? delta.delay_ms : 0,
|
|
439
|
+
runId,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (messageType === "stop_reason" || messageType === "ping") {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
type: "stream_event",
|
|
449
|
+
event: delta as SDKStreamEventPayload,
|
|
450
|
+
uuid,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private resultFromTurn(
|
|
455
|
+
turn: RuntimeTurnResult,
|
|
456
|
+
tracker?: TurnTracker,
|
|
457
|
+
): SDKResultMessage {
|
|
458
|
+
const stopReason =
|
|
459
|
+
turn.stopReason ?? (turn.success === false ? "error" : undefined);
|
|
460
|
+
const approvalConflict = isApprovalConflictSignal({
|
|
461
|
+
detail: turn.detail,
|
|
462
|
+
stopReason,
|
|
463
|
+
});
|
|
464
|
+
const success =
|
|
465
|
+
turn.success !== undefined
|
|
466
|
+
? turn.success &&
|
|
467
|
+
!approvalConflict &&
|
|
468
|
+
!FAILURE_STOP_REASONS.has(stopReason ?? "")
|
|
469
|
+
: !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "");
|
|
470
|
+
const errorCode = approvalConflict
|
|
471
|
+
? "approval_conflict"
|
|
472
|
+
: (turn.errorCode ?? toSdkErrorCode(stopReason));
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
type: "result",
|
|
476
|
+
success,
|
|
477
|
+
result: success ? tracker?.assistantText || undefined : undefined,
|
|
478
|
+
error: success ? undefined : (errorCode ?? stopReason ?? "error"),
|
|
479
|
+
errorCode: success ? undefined : (errorCode ?? "error"),
|
|
480
|
+
approvalConflict: approvalConflict || undefined,
|
|
481
|
+
recoverable: approvalConflict ? true : success ? undefined : false,
|
|
482
|
+
errorDetail: success ? undefined : turn.detail,
|
|
483
|
+
stopReason,
|
|
484
|
+
durationMs:
|
|
485
|
+
Date.now() - (tracker?.startedAt || this._activeTurnStartedAt),
|
|
486
|
+
conversationId: turn.runtime.conversation_id,
|
|
487
|
+
runIds: turn.runIds.length > 0 ? turn.runIds : undefined,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private enqueue(message: SDKMessage): void {
|
|
492
|
+
const resolver = this.streamResolvers.shift();
|
|
493
|
+
if (resolver) {
|
|
494
|
+
resolver(message);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
this.streamQueue.push(message);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private resolveAll(value: SDKMessage | null): void {
|
|
501
|
+
for (const resolve of this.streamResolvers.splice(0)) {
|
|
502
|
+
resolve(value);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
package/src/remote.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export type RemoteEnvironmentTarget =
|
|
2
|
+
| { connectionId: string }
|
|
3
|
+
| { environmentId: string }
|
|
4
|
+
| { deviceId: string }
|
|
5
|
+
| { connectionName: string };
|
|
6
|
+
|
|
7
|
+
export interface RemoteEnvironmentClientOptions {
|
|
8
|
+
/** Letta API base URL. Defaults to https://api.letta.com. */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/** Bearer token / API key. Defaults to process.env.LETTA_API_KEY when present. */
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
/** Additional headers, e.g. x-project-id. */
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
/** Custom fetch implementation for tests or non-standard runtimes. */
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RemoteEnvironmentConnection {
|
|
19
|
+
id: string;
|
|
20
|
+
connectionId: string | null;
|
|
21
|
+
deviceId: string;
|
|
22
|
+
connectionName: string;
|
|
23
|
+
organizationId: string;
|
|
24
|
+
userId?: string;
|
|
25
|
+
apiKeyOwner?: string;
|
|
26
|
+
podId: string | null;
|
|
27
|
+
connectedAt: number | null;
|
|
28
|
+
lastHeartbeat: number | null;
|
|
29
|
+
lastSeenAt: number;
|
|
30
|
+
firstSeenAt: number;
|
|
31
|
+
currentMode?: string;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RemoteEnvironmentListResult {
|
|
36
|
+
connections: RemoteEnvironmentConnection[];
|
|
37
|
+
hasNextPage: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ResolvedRemoteEnvironment {
|
|
41
|
+
connectionId: string;
|
|
42
|
+
environment?: RemoteEnvironmentConnection;
|
|
43
|
+
target: RemoteEnvironmentTarget;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type RemoteFetch = typeof fetch;
|
|
47
|
+
|
|
48
|
+
function getDefaultApiKey(): string | undefined {
|
|
49
|
+
if (typeof process === "undefined") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return process.env.LETTA_API_KEY;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createHeaders(options: RemoteEnvironmentClientOptions): Record<string, string> {
|
|
56
|
+
const apiKey = options.apiKey ?? getDefaultApiKey();
|
|
57
|
+
return {
|
|
58
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
59
|
+
...(options.headers ?? {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getFetch(options: RemoteEnvironmentClientOptions): RemoteFetch {
|
|
64
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
65
|
+
if (!fetchImpl) {
|
|
66
|
+
throw new Error("Remote environments require a fetch implementation");
|
|
67
|
+
}
|
|
68
|
+
return fetchImpl.bind(globalThis) as RemoteFetch;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeBaseUrl(baseUrl?: string): string {
|
|
72
|
+
return (baseUrl ?? "https://api.letta.com").replace(/\/$/, "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ensureOnline(
|
|
76
|
+
environment: RemoteEnvironmentConnection,
|
|
77
|
+
target: RemoteEnvironmentTarget,
|
|
78
|
+
): ResolvedRemoteEnvironment {
|
|
79
|
+
if (!environment.connectionId) {
|
|
80
|
+
const label =
|
|
81
|
+
"deviceId" in target
|
|
82
|
+
? target.deviceId
|
|
83
|
+
: "environmentId" in target
|
|
84
|
+
? target.environmentId
|
|
85
|
+
: "connectionName" in target
|
|
86
|
+
? target.connectionName
|
|
87
|
+
: environment.deviceId;
|
|
88
|
+
throw new Error(`Remote environment is offline: ${label}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
connectionId: environment.connectionId,
|
|
93
|
+
environment,
|
|
94
|
+
target,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function parseJsonResponse<T>(response: Response): Promise<T> {
|
|
99
|
+
const text = await response.text();
|
|
100
|
+
const body = text ? JSON.parse(text) : null;
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const message =
|
|
104
|
+
body && typeof body === "object" && "message" in body
|
|
105
|
+
? String((body as { message: unknown }).message)
|
|
106
|
+
: response.statusText;
|
|
107
|
+
throw new Error(`Letta API request failed (${response.status}): ${message}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return body as T;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Small Cloud API helper for resolving explicit Letta Code environments.
|
|
115
|
+
*/
|
|
116
|
+
export class RemoteEnvironmentClient {
|
|
117
|
+
private readonly baseUrl: string;
|
|
118
|
+
private readonly fetchImpl: RemoteFetch;
|
|
119
|
+
|
|
120
|
+
constructor(private readonly options: RemoteEnvironmentClientOptions = {}) {
|
|
121
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
122
|
+
this.fetchImpl = getFetch(options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async listEnvironments(): Promise<RemoteEnvironmentListResult> {
|
|
126
|
+
const url = new URL(`${this.baseUrl}/v1/environments`);
|
|
127
|
+
const response = await this.fetchImpl(url, {
|
|
128
|
+
headers: createHeaders(this.options),
|
|
129
|
+
});
|
|
130
|
+
return parseJsonResponse<RemoteEnvironmentListResult>(response);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getEnvironmentByDeviceId(deviceId: string): Promise<RemoteEnvironmentConnection> {
|
|
134
|
+
const response = await this.fetchImpl(
|
|
135
|
+
`${this.baseUrl}/v1/environments/${encodeURIComponent(deviceId)}`,
|
|
136
|
+
{ headers: createHeaders(this.options) },
|
|
137
|
+
);
|
|
138
|
+
return parseJsonResponse<RemoteEnvironmentConnection>(response);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async resolveEnvironment(target: RemoteEnvironmentTarget): Promise<ResolvedRemoteEnvironment> {
|
|
142
|
+
if ("connectionId" in target) {
|
|
143
|
+
return { connectionId: target.connectionId, target };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if ("deviceId" in target) {
|
|
147
|
+
return ensureOnline(await this.getEnvironmentByDeviceId(target.deviceId), target);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { connections } = await this.listEnvironments();
|
|
151
|
+
if ("environmentId" in target) {
|
|
152
|
+
const match = connections.find((env) => env.id === target.environmentId);
|
|
153
|
+
if (!match) {
|
|
154
|
+
throw new Error(`Remote environment not found: ${target.environmentId}`);
|
|
155
|
+
}
|
|
156
|
+
return ensureOnline(match, target);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const matches = connections.filter(
|
|
160
|
+
(env) => env.connectionName === target.connectionName,
|
|
161
|
+
);
|
|
162
|
+
if (matches.length === 0) {
|
|
163
|
+
throw new Error(`Remote environment not found: ${target.connectionName}`);
|
|
164
|
+
}
|
|
165
|
+
if (matches.length > 1) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Remote environment name is ambiguous: ${target.connectionName}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const match = matches[0];
|
|
171
|
+
if (!match) {
|
|
172
|
+
throw new Error(`Remote environment not found: ${target.connectionName}`);
|
|
173
|
+
}
|
|
174
|
+
return ensureOnline(match, target);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
}
|