@letta-ai/letta-code 0.30.18 → 0.30.20
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/dist/gateway-core.js +604 -31
- package/dist/gateway-core.js.map +6 -4
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/types/agent/model.d.ts.map +1 -1
- package/dist/types/agent/subagents/subagent-launcher.d.ts.map +1 -1
- package/dist/types/channels/control-request-coordinator.d.ts +42 -0
- package/dist/types/channels/control-request-coordinator.d.ts.map +1 -0
- package/dist/types/channels/gateway-core.d.ts +4 -0
- package/dist/types/channels/gateway-core.d.ts.map +1 -1
- package/dist/types/channels/interactive.d.ts +13 -0
- package/dist/types/channels/interactive.d.ts.map +1 -0
- package/dist/types/gateway-core.d.ts +3 -0
- package/dist/types/gateway-core.d.ts.map +1 -1
- package/dist/types/mods/capabilities.d.ts +4 -0
- package/dist/types/mods/capabilities.d.ts.map +1 -1
- package/dist/types/mods/mod-adapter.d.ts.map +1 -1
- package/dist/types/tools/impl/bash.d.ts.map +1 -1
- package/dist/types/tools/impl/foreground-sleep.d.ts +13 -0
- package/dist/types/tools/impl/foreground-sleep.d.ts.map +1 -0
- package/dist/types/tools/impl/task-update.d.ts +1 -1
- package/dist/types/tools/impl/task-update.d.ts.map +1 -1
- package/dist/types/tools/impl/task.d.ts +1 -0
- package/dist/types/tools/impl/task.d.ts.map +1 -1
- package/dist/types/tools/impl/tasks/store.d.ts +3 -6
- package/dist/types/tools/impl/tasks/store.d.ts.map +1 -1
- package/dist/types/types/app-server-protocol.d.ts +1 -0
- package/dist/types/types/app-server-protocol.d.ts.map +1 -1
- package/dist/types/types/protocol_v2.d.ts +11 -13
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/types/queue-update-protocol.d.ts +5 -0
- package/dist/types/types/queue-update-protocol.d.ts.map +1 -0
- package/dist/types/types/service-protocol.d.ts +11 -0
- package/dist/types/types/service-protocol.d.ts.map +1 -1
- package/dist/types/types/teleport-protocol.d.ts +54 -0
- package/dist/types/types/teleport-protocol.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound.d.ts +3 -6
- package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
- package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
- package/dist/types/websocket/listener/types.d.ts +13 -1
- package/dist/types/websocket/listener/types.d.ts.map +1 -1
- package/letta.js +2572 -1275
- package/package.json +1 -1
- package/scripts/source-file-size-baseline.json +5 -5
- package/skills/scheduling-tasks/SKILL.md +10 -8
- package/skills/teleporting-between-environments/SKILL.md +100 -0
package/dist/gateway-core.js
CHANGED
|
@@ -1,3 +1,490 @@
|
|
|
1
|
+
// src/channels/interactive.ts
|
|
2
|
+
function normalizeWhitespace(text) {
|
|
3
|
+
return text.replace(/\s+/g, " ").trim();
|
|
4
|
+
}
|
|
5
|
+
function isAffirmativeResponse(text) {
|
|
6
|
+
const normalized = normalizeWhitespace(text).toLowerCase();
|
|
7
|
+
return [
|
|
8
|
+
"approve",
|
|
9
|
+
"approved",
|
|
10
|
+
"allow",
|
|
11
|
+
"yes",
|
|
12
|
+
"y",
|
|
13
|
+
"ok",
|
|
14
|
+
"okay",
|
|
15
|
+
"continue",
|
|
16
|
+
"go ahead",
|
|
17
|
+
"looks good",
|
|
18
|
+
"lgtm",
|
|
19
|
+
"sgtm",
|
|
20
|
+
"ship it"
|
|
21
|
+
].includes(normalized);
|
|
22
|
+
}
|
|
23
|
+
function stripApprovalPrefix(text) {
|
|
24
|
+
return normalizeWhitespace(text.replace(/^(approve|allow|yes|y|ok|okay|deny|reject|no|n)\s*[:-]?\s*/i, ""));
|
|
25
|
+
}
|
|
26
|
+
function summarizeControlRequestInput(input) {
|
|
27
|
+
const serialized = JSON.stringify(input, null, 2);
|
|
28
|
+
if (!serialized || serialized === "{}") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (serialized.length <= 1200) {
|
|
32
|
+
return serialized;
|
|
33
|
+
}
|
|
34
|
+
return `${serialized.slice(0, 1197).trimEnd()}...`;
|
|
35
|
+
}
|
|
36
|
+
function buildQuestionPrompt(question, index) {
|
|
37
|
+
const lines = [
|
|
38
|
+
`${index + 1}. ${question.question ?? `Question ${index + 1}`}`
|
|
39
|
+
];
|
|
40
|
+
const options = question.options ?? [];
|
|
41
|
+
options.forEach((option, optionIndex) => {
|
|
42
|
+
const label = option.label?.trim() || `Option ${optionIndex + 1}`;
|
|
43
|
+
const description = option.description?.trim();
|
|
44
|
+
lines.push(description ? ` ${optionIndex + 1}) ${label} — ${description}` : ` ${optionIndex + 1}) ${label}`);
|
|
45
|
+
});
|
|
46
|
+
if (question.multiSelect) {
|
|
47
|
+
lines.push(" Choose one or more options. Separate multiple answers with commas.");
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
function matchQuestionOption(question, text) {
|
|
52
|
+
const trimmed = normalizeWhitespace(text);
|
|
53
|
+
const options = question.options ?? [];
|
|
54
|
+
if (!trimmed || options.length === 0) {
|
|
55
|
+
return trimmed;
|
|
56
|
+
}
|
|
57
|
+
const numberMatch = trimmed.match(/^(\d+)$/);
|
|
58
|
+
if (numberMatch?.[1]) {
|
|
59
|
+
const option = options[Number(numberMatch[1]) - 1];
|
|
60
|
+
if (option?.label?.trim()) {
|
|
61
|
+
return option.label.trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const exactLabel = options.find((option) => option.label && normalizeWhitespace(option.label).toLowerCase() === trimmed.toLowerCase());
|
|
65
|
+
if (exactLabel?.label?.trim()) {
|
|
66
|
+
return exactLabel.label.trim();
|
|
67
|
+
}
|
|
68
|
+
const trimmedLower = trimmed.toLowerCase();
|
|
69
|
+
const exactAlias = options.find((option) => optionAliases(option).some((alias) => normalizeWhitespace(alias).toLowerCase() === trimmedLower));
|
|
70
|
+
if (exactAlias?.label?.trim()) {
|
|
71
|
+
return exactAlias.label.trim();
|
|
72
|
+
}
|
|
73
|
+
if (/^(recommended|default|defaults)$/i.test(trimmed)) {
|
|
74
|
+
const recommended = options.find((option) => {
|
|
75
|
+
const label = option.label?.toLowerCase() ?? "";
|
|
76
|
+
const description = option.description?.toLowerCase() ?? "";
|
|
77
|
+
return label.includes("recommended") || description.includes("recommended");
|
|
78
|
+
});
|
|
79
|
+
if (recommended?.label?.trim()) {
|
|
80
|
+
return recommended.label.trim();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return trimmed;
|
|
84
|
+
}
|
|
85
|
+
function escapeRegExp(text) {
|
|
86
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
87
|
+
}
|
|
88
|
+
function optionAliases(option) {
|
|
89
|
+
const aliases = new Set;
|
|
90
|
+
const label = normalizeWhitespace(option.label ?? "");
|
|
91
|
+
if (label) {
|
|
92
|
+
aliases.add(label);
|
|
93
|
+
const withoutParenthetical = normalizeWhitespace(label.replace(/\s*\([^)]*\)\s*/g, " "));
|
|
94
|
+
if (withoutParenthetical) {
|
|
95
|
+
aliases.add(withoutParenthetical);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return Array.from(aliases).filter((alias) => alias.length >= 3);
|
|
99
|
+
}
|
|
100
|
+
function containsPhrase(text, phrase) {
|
|
101
|
+
const normalizedText = normalizeWhitespace(text);
|
|
102
|
+
const normalizedPhrase = normalizeWhitespace(phrase);
|
|
103
|
+
if (!normalizedText || !normalizedPhrase) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const escapedPhrase = escapeRegExp(normalizedPhrase).replace(/\s+/g, "\\s+");
|
|
107
|
+
return new RegExp(`(^|\\W)${escapedPhrase}(?=$|\\W)`, "i").test(normalizedText);
|
|
108
|
+
}
|
|
109
|
+
function inferQuestionAnswerFromText(question, rawText) {
|
|
110
|
+
const options = question.options ?? [];
|
|
111
|
+
const matches = options.filter((option) => optionAliases(option).some((alias) => containsPhrase(rawText, alias))).map((option) => option.label?.trim()).filter((label) => Boolean(label));
|
|
112
|
+
const uniqueMatches = Array.from(new Set(matches));
|
|
113
|
+
if (uniqueMatches.length === 1) {
|
|
114
|
+
return uniqueMatches[0] ?? null;
|
|
115
|
+
}
|
|
116
|
+
const normalized = normalizeWhitespace(rawText).toLowerCase();
|
|
117
|
+
if (/\b(recommended|default|defaults|you decide|whatever you recommend)\b/.test(normalized)) {
|
|
118
|
+
const recommended = options.find((option) => {
|
|
119
|
+
const label = option.label?.toLowerCase() ?? "";
|
|
120
|
+
const description = option.description?.toLowerCase() ?? "";
|
|
121
|
+
return label.includes("recommended") || description.includes("recommended");
|
|
122
|
+
});
|
|
123
|
+
if (recommended?.label?.trim()) {
|
|
124
|
+
return recommended.label.trim();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
function matchQuestionAnswer(question, text) {
|
|
130
|
+
if (!question.multiSelect) {
|
|
131
|
+
return matchQuestionOption(question, text);
|
|
132
|
+
}
|
|
133
|
+
const normalized = normalizeWhitespace(text);
|
|
134
|
+
if (!normalized) {
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
137
|
+
const selections = normalized.replace(/\band\b/gi, ",").split(/\s*(?:,|\/|;)\s*/).map((entry) => normalizeWhitespace(entry)).filter(Boolean);
|
|
138
|
+
if (selections.length <= 1) {
|
|
139
|
+
return matchQuestionOption(question, normalized);
|
|
140
|
+
}
|
|
141
|
+
const matchedSelections = Array.from(new Set(selections.map((selection) => matchQuestionOption(question, selection)).filter(Boolean)));
|
|
142
|
+
return matchedSelections.length > 0 ? matchedSelections.join(", ") : normalized;
|
|
143
|
+
}
|
|
144
|
+
function parseNumberedAnswers(rawText, questions) {
|
|
145
|
+
const matches = Array.from(rawText.matchAll(/(?:^|\n)\s*(\d+)[).:-]\s*(.+?)(?=(?:\n\s*\d+[).:-]\s*)|$)/gs));
|
|
146
|
+
if (matches.length === 0) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const answers = {};
|
|
150
|
+
for (const match of matches) {
|
|
151
|
+
const questionIndex = Number(match[1]) - 1;
|
|
152
|
+
const question = questions[questionIndex];
|
|
153
|
+
const answerText = match[2]?.trim();
|
|
154
|
+
if (!question?.question || !answerText) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
answers[question.question] = matchQuestionAnswer(question, answerText);
|
|
158
|
+
}
|
|
159
|
+
return Object.keys(answers).length > 0 ? answers : null;
|
|
160
|
+
}
|
|
161
|
+
function parsePositionalAnswers(rawText, questions) {
|
|
162
|
+
const lines = rawText.split(/\r?\n/).map((line) => normalizeWhitespace(line)).filter(Boolean);
|
|
163
|
+
if (lines.length === questions.length) {
|
|
164
|
+
const answers = {};
|
|
165
|
+
questions.forEach((question, index) => {
|
|
166
|
+
if (question.question) {
|
|
167
|
+
answers[question.question] = matchQuestionAnswer(question, lines[index] ?? "");
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return answers;
|
|
171
|
+
}
|
|
172
|
+
const normalized = normalizeWhitespace(rawText);
|
|
173
|
+
const compactParts = normalized.split(/\s*(?:,|\/|;)\s*/).map((part) => normalizeWhitespace(part)).filter(Boolean);
|
|
174
|
+
if (compactParts.length === questions.length && compactParts.every((part) => /^\d+$/.test(part))) {
|
|
175
|
+
const answers = {};
|
|
176
|
+
questions.forEach((question, index) => {
|
|
177
|
+
if (question.question) {
|
|
178
|
+
answers[question.question] = matchQuestionAnswer(question, compactParts[index] ?? "");
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return answers;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function parseFlexibleMultiQuestionAnswers(rawText, questions) {
|
|
186
|
+
const normalized = normalizeWhitespace(rawText);
|
|
187
|
+
if (!normalized) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const positionalAnswers = parsePositionalAnswers(rawText, questions);
|
|
191
|
+
if (positionalAnswers) {
|
|
192
|
+
return positionalAnswers;
|
|
193
|
+
}
|
|
194
|
+
const answers = {};
|
|
195
|
+
for (const question of questions) {
|
|
196
|
+
if (!question.question) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const inferred = inferQuestionAnswerFromText(question, rawText);
|
|
200
|
+
if (inferred) {
|
|
201
|
+
answers[question.question] = inferred;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const hasInferredAnswers = Object.keys(answers).length > 0;
|
|
205
|
+
for (const question of questions) {
|
|
206
|
+
if (!question.question || Object.hasOwn(answers, question.question)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
answers[question.question] = hasInferredAnswers ? `Not specified. Full user reply: ${normalized}` : normalized;
|
|
210
|
+
}
|
|
211
|
+
return Object.keys(answers).length > 0 ? answers : null;
|
|
212
|
+
}
|
|
213
|
+
function buildAllowResponse(requestId, decision) {
|
|
214
|
+
return {
|
|
215
|
+
request_id: requestId,
|
|
216
|
+
decision
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function buildDenyResponse(requestId, message) {
|
|
220
|
+
return {
|
|
221
|
+
request_id: requestId,
|
|
222
|
+
decision: {
|
|
223
|
+
behavior: "deny",
|
|
224
|
+
message
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function getAskUserQuestionInput(input) {
|
|
229
|
+
return input;
|
|
230
|
+
}
|
|
231
|
+
function formatAskUserQuestionPrompt(event) {
|
|
232
|
+
const input = getAskUserQuestionInput(event.input);
|
|
233
|
+
const questions = (input.questions ?? []).filter((question) => normalizeWhitespace(question.question ?? ""));
|
|
234
|
+
const lines = [
|
|
235
|
+
"SYSTEM MESSAGE — reply required to continue",
|
|
236
|
+
"",
|
|
237
|
+
...questions.flatMap((question, index) => buildQuestionPrompt(question, index)),
|
|
238
|
+
""
|
|
239
|
+
];
|
|
240
|
+
if (questions.length <= 1) {
|
|
241
|
+
const singleQuestion = questions[0];
|
|
242
|
+
lines.push(singleQuestion?.multiSelect ? "Reply with one or more option numbers/labels separated by commas, or just send a freeform answer in your next message." : "Reply with an option number/label, or just send a freeform answer in your next message.");
|
|
243
|
+
} else {
|
|
244
|
+
lines.push("Reply in plain language, or answer each question separately with numbered lines:", "1: your answer", "2: your answer", "", "Option numbers/labels work too. For multi-select questions, separate multiple answers with commas.");
|
|
245
|
+
}
|
|
246
|
+
return lines.join(`
|
|
247
|
+
`);
|
|
248
|
+
}
|
|
249
|
+
function formatGenericToolApprovalPrompt(event) {
|
|
250
|
+
const inputSummary = summarizeControlRequestInput(event.input);
|
|
251
|
+
const lines = [`The agent wants approval to run \`${event.toolName}\`.`];
|
|
252
|
+
if (inputSummary) {
|
|
253
|
+
lines.push("", "Tool input:", inputSummary);
|
|
254
|
+
}
|
|
255
|
+
lines.push("", "Reply `approve` to allow it.", "Reply with feedback instead if you want to deny it.");
|
|
256
|
+
return lines.join(`
|
|
257
|
+
`);
|
|
258
|
+
}
|
|
259
|
+
function formatChannelControlRequestPrompt(event) {
|
|
260
|
+
switch (event.kind) {
|
|
261
|
+
case "ask_user_question":
|
|
262
|
+
return formatAskUserQuestionPrompt(event);
|
|
263
|
+
case "generic_tool_approval":
|
|
264
|
+
return formatGenericToolApprovalPrompt(event);
|
|
265
|
+
default: {
|
|
266
|
+
const exhaustiveCheck = event.kind;
|
|
267
|
+
return exhaustiveCheck;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function parseAskUserQuestionResponse(event, rawText) {
|
|
272
|
+
const input = getAskUserQuestionInput(event.input);
|
|
273
|
+
const questions = (input.questions ?? []).filter((question) => normalizeWhitespace(question.question ?? ""));
|
|
274
|
+
if (questions.length === 0) {
|
|
275
|
+
return {
|
|
276
|
+
type: "reprompt",
|
|
277
|
+
message: "I couldn't find the original question payload. Please ask the agent to try again."
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (questions.length === 1) {
|
|
281
|
+
const [question] = questions;
|
|
282
|
+
if (!question?.question) {
|
|
283
|
+
return {
|
|
284
|
+
type: "reprompt",
|
|
285
|
+
message: "I couldn't find the original question text. Please ask the agent to try again."
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const answer = matchQuestionAnswer(question, rawText);
|
|
289
|
+
return {
|
|
290
|
+
type: "response",
|
|
291
|
+
response: buildAllowResponse(event.requestId, {
|
|
292
|
+
behavior: "allow",
|
|
293
|
+
updated_input: {
|
|
294
|
+
...event.input,
|
|
295
|
+
answers: {
|
|
296
|
+
...input.answers ?? {},
|
|
297
|
+
[question.question]: answer
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const numberedAnswers = parseNumberedAnswers(rawText, questions);
|
|
304
|
+
const answers = numberedAnswers ?? parseFlexibleMultiQuestionAnswers(rawText, questions);
|
|
305
|
+
if (!answers) {
|
|
306
|
+
return {
|
|
307
|
+
type: "reprompt",
|
|
308
|
+
message: "Please send a reply so I can pass it back to the agent. You can answer naturally or use numbered lines like `1: ...` and `2: ...`."
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
type: "response",
|
|
313
|
+
response: buildAllowResponse(event.requestId, {
|
|
314
|
+
behavior: "allow",
|
|
315
|
+
updated_input: {
|
|
316
|
+
...event.input,
|
|
317
|
+
answers: {
|
|
318
|
+
...input.answers ?? {},
|
|
319
|
+
...answers
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function parseGenericToolApprovalResponse(event, rawText) {
|
|
326
|
+
if (isAffirmativeResponse(rawText)) {
|
|
327
|
+
const message = stripApprovalPrefix(rawText);
|
|
328
|
+
return {
|
|
329
|
+
type: "response",
|
|
330
|
+
response: buildAllowResponse(event.requestId, {
|
|
331
|
+
behavior: "allow",
|
|
332
|
+
...message ? { message } : {}
|
|
333
|
+
})
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const feedback = stripApprovalPrefix(rawText);
|
|
337
|
+
return {
|
|
338
|
+
type: "response",
|
|
339
|
+
response: buildDenyResponse(event.requestId, feedback || "Denied by channel user.")
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function parseChannelControlRequestResponse(event, rawText) {
|
|
343
|
+
const trimmed = rawText.trim();
|
|
344
|
+
if (!trimmed) {
|
|
345
|
+
return {
|
|
346
|
+
type: "reprompt",
|
|
347
|
+
message: formatChannelControlRequestPrompt(event)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
switch (event.kind) {
|
|
351
|
+
case "ask_user_question":
|
|
352
|
+
return parseAskUserQuestionResponse(event, trimmed);
|
|
353
|
+
case "generic_tool_approval":
|
|
354
|
+
return parseGenericToolApprovalResponse(event, trimmed);
|
|
355
|
+
default: {
|
|
356
|
+
const exhaustiveCheck = event.kind;
|
|
357
|
+
return exhaustiveCheck;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/channels/control-request-coordinator.ts
|
|
363
|
+
function getChannelControlRequestScopeKey(params) {
|
|
364
|
+
return [
|
|
365
|
+
params.channel,
|
|
366
|
+
params.accountId ?? "default",
|
|
367
|
+
params.chatId,
|
|
368
|
+
params.threadId ?? ""
|
|
369
|
+
].join(":");
|
|
370
|
+
}
|
|
371
|
+
function cloneEvent(event) {
|
|
372
|
+
return structuredClone(event);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
class ChannelControlRequestCoordinator {
|
|
376
|
+
options;
|
|
377
|
+
pendingById = new Map;
|
|
378
|
+
requestIdByScope = new Map;
|
|
379
|
+
constructor(options) {
|
|
380
|
+
this.options = options;
|
|
381
|
+
}
|
|
382
|
+
restore(events) {
|
|
383
|
+
for (const event of events) {
|
|
384
|
+
this.remember(event, false);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
has(requestId) {
|
|
388
|
+
return this.pendingById.has(requestId);
|
|
389
|
+
}
|
|
390
|
+
getAll() {
|
|
391
|
+
return Array.from(this.pendingById.values()).map((pending) => ({
|
|
392
|
+
event: cloneEvent(pending.event),
|
|
393
|
+
deliveredThisProcess: pending.deliveredThisProcess
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
async register(event) {
|
|
397
|
+
const scopeKey = getChannelControlRequestScopeKey(event.source);
|
|
398
|
+
const existingRequestId = this.requestIdByScope.get(scopeKey);
|
|
399
|
+
if (existingRequestId && existingRequestId !== event.requestId) {
|
|
400
|
+
await this.clear(existingRequestId);
|
|
401
|
+
}
|
|
402
|
+
this.remember(event, false);
|
|
403
|
+
await this.options.persist(cloneEvent(event));
|
|
404
|
+
await this.deliver(event.requestId);
|
|
405
|
+
}
|
|
406
|
+
async redeliver(requestId) {
|
|
407
|
+
return this.deliver(requestId);
|
|
408
|
+
}
|
|
409
|
+
async handleNativeResponse(input) {
|
|
410
|
+
const pending = this.pendingById.get(input.requestId);
|
|
411
|
+
if (!pending)
|
|
412
|
+
return "expired";
|
|
413
|
+
const source = pending.event.source;
|
|
414
|
+
if (source.channel !== input.channel || (source.accountId ?? "default") !== (input.accountId ?? "default") || source.chatId !== input.chatId || (source.threadId ?? null) !== (input.threadId ?? null) || source.senderId && source.senderId !== input.senderId) {
|
|
415
|
+
return "forbidden";
|
|
416
|
+
}
|
|
417
|
+
const result = await this.options.deliverResponse(cloneEvent(pending.event), input.response);
|
|
418
|
+
if (result === "handled" || result === "expired") {
|
|
419
|
+
await this.clear(input.requestId);
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
async tryHandleInbound(input) {
|
|
424
|
+
if (input.bypass)
|
|
425
|
+
return false;
|
|
426
|
+
const requestId = this.requestIdByScope.get(getChannelControlRequestScopeKey(input));
|
|
427
|
+
if (!requestId)
|
|
428
|
+
return false;
|
|
429
|
+
const pending = this.pendingById.get(requestId);
|
|
430
|
+
if (!pending) {
|
|
431
|
+
this.requestIdByScope.delete(getChannelControlRequestScopeKey(input));
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
if (pending.event.source.senderId && pending.event.source.senderId !== input.senderId) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
if (input.channel === "slack" && pending.event.kind === "generic_tool_approval") {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
const parsed = parseChannelControlRequestResponse(pending.event, input.text);
|
|
441
|
+
if (parsed.type === "reprompt") {
|
|
442
|
+
await this.options.deliverReprompt(cloneEvent(pending.event), input, parsed.message);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
const result = await this.options.deliverResponse(cloneEvent(pending.event), parsed.response);
|
|
446
|
+
if (result === "unavailable") {
|
|
447
|
+
await this.options.deliverReprompt(cloneEvent(pending.event), input, "I’m reconnecting to Letta Code right now, so I couldn’t use that reply yet. Please send it again in a moment.");
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
await this.clear(requestId);
|
|
451
|
+
if (result === "expired") {
|
|
452
|
+
await this.options.deliverReprompt(cloneEvent(pending.event), input, "That approval prompt expired before I could use your reply. Please ask the agent to try again.");
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
async clear(requestId) {
|
|
457
|
+
const pending = this.pendingById.get(requestId);
|
|
458
|
+
if (pending) {
|
|
459
|
+
this.pendingById.delete(requestId);
|
|
460
|
+
const scopeKey = getChannelControlRequestScopeKey(pending.event.source);
|
|
461
|
+
if (this.requestIdByScope.get(scopeKey) === requestId) {
|
|
462
|
+
this.requestIdByScope.delete(scopeKey);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
await this.options.remove(requestId);
|
|
466
|
+
}
|
|
467
|
+
clearAll() {
|
|
468
|
+
this.pendingById.clear();
|
|
469
|
+
this.requestIdByScope.clear();
|
|
470
|
+
}
|
|
471
|
+
remember(event, deliveredThisProcess) {
|
|
472
|
+
const nextEvent = cloneEvent(event);
|
|
473
|
+
this.pendingById.set(event.requestId, {
|
|
474
|
+
event: nextEvent,
|
|
475
|
+
deliveredThisProcess
|
|
476
|
+
});
|
|
477
|
+
this.requestIdByScope.set(getChannelControlRequestScopeKey(event.source), event.requestId);
|
|
478
|
+
}
|
|
479
|
+
async deliver(requestId) {
|
|
480
|
+
const pending = this.pendingById.get(requestId);
|
|
481
|
+
if (!pending)
|
|
482
|
+
return false;
|
|
483
|
+
await this.options.deliverPrompt(cloneEvent(pending.event));
|
|
484
|
+
pending.deliveredThisProcess = true;
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
1
488
|
// src/tools/interactive-policy.ts
|
|
2
489
|
var INTERACTIVE_APPROVAL_TOOLS = new Set(["AskUserQuestion"]);
|
|
3
490
|
var INTERACTIVE_USER_INPUT_TOOL_NAMES = [
|
|
@@ -846,7 +1333,7 @@ var MAX_ACCEPTED_CLIENT_MESSAGE_IDS = 2048;
|
|
|
846
1333
|
function runtimeKey(runtime) {
|
|
847
1334
|
return `${runtime.agent_id}:${runtime.conversation_id}`;
|
|
848
1335
|
}
|
|
849
|
-
function
|
|
1336
|
+
function sourceRouteKey(source) {
|
|
850
1337
|
return [
|
|
851
1338
|
source.channel,
|
|
852
1339
|
source.accountId ?? "",
|
|
@@ -854,12 +1341,26 @@ function sourceKey(source) {
|
|
|
854
1341
|
source.threadId ?? ""
|
|
855
1342
|
].join(":");
|
|
856
1343
|
}
|
|
857
|
-
function
|
|
1344
|
+
function sourceLifecycleKey(source) {
|
|
1345
|
+
return [
|
|
1346
|
+
sourceRouteKey(source),
|
|
1347
|
+
source.messageId ?? "",
|
|
1348
|
+
source.agentId,
|
|
1349
|
+
source.conversationId
|
|
1350
|
+
].join(":");
|
|
1351
|
+
}
|
|
1352
|
+
function uniqueSourcesBy(sources, getKey) {
|
|
858
1353
|
const byKey = new Map;
|
|
859
1354
|
for (const source of sources)
|
|
860
|
-
byKey.set(
|
|
1355
|
+
byKey.set(getKey(source), source);
|
|
861
1356
|
return [...byKey.values()];
|
|
862
1357
|
}
|
|
1358
|
+
function uniqueRoutedSources(sources) {
|
|
1359
|
+
return uniqueSourcesBy(sources, sourceRouteKey);
|
|
1360
|
+
}
|
|
1361
|
+
function uniqueLifecycleSources(sources) {
|
|
1362
|
+
return uniqueSourcesBy(sources, sourceLifecycleKey);
|
|
1363
|
+
}
|
|
863
1364
|
function channelTagsForSources(sources) {
|
|
864
1365
|
return [...new Set(sources.map((source) => `channel:${source.channel}`))];
|
|
865
1366
|
}
|
|
@@ -892,7 +1393,7 @@ class ChannelGateway {
|
|
|
892
1393
|
this.disposers.push(client.onMessage((message) => this.handleMessage(message)), client.onExternalToolCall((request) => {
|
|
893
1394
|
const state = request.runtime ? this.states.get(runtimeKey(request.runtime)) : undefined;
|
|
894
1395
|
const active = state?.active;
|
|
895
|
-
const sources = active?.
|
|
1396
|
+
const sources = active?.routingSources ?? state?.routedSources ?? [];
|
|
896
1397
|
return hooks.executeExternalTool(request, sources, active?.idempotencyScope ?? null);
|
|
897
1398
|
}));
|
|
898
1399
|
}
|
|
@@ -919,12 +1420,12 @@ class ChannelGateway {
|
|
|
919
1420
|
return true;
|
|
920
1421
|
}
|
|
921
1422
|
state.pendingSourcesByClientMessageId.set(delivery.clientMessageId, {
|
|
922
|
-
sources:
|
|
1423
|
+
sources: uniqueLifecycleSources(delivery.sources),
|
|
923
1424
|
disposition: "submitting"
|
|
924
1425
|
});
|
|
925
1426
|
try {
|
|
926
1427
|
await this.enqueueRegistration(async () => {
|
|
927
|
-
state.routedSources =
|
|
1428
|
+
state.routedSources = uniqueRoutedSources([
|
|
928
1429
|
...state.routedSources,
|
|
929
1430
|
...delivery.sources
|
|
930
1431
|
]);
|
|
@@ -962,8 +1463,8 @@ class ChannelGateway {
|
|
|
962
1463
|
const pending = state.pendingSourcesByClientMessageId.get(delivery.clientMessageId);
|
|
963
1464
|
if (pending) {
|
|
964
1465
|
pending.disposition = "queued";
|
|
965
|
-
pending.acceptedAtQueueRevision = state.queueRevision;
|
|
966
1466
|
}
|
|
1467
|
+
this.reconcileExplicitQueueRemovals(state);
|
|
967
1468
|
}
|
|
968
1469
|
await Promise.all(queuedEvents);
|
|
969
1470
|
return true;
|
|
@@ -979,7 +1480,8 @@ class ChannelGateway {
|
|
|
979
1480
|
if (!state.active) {
|
|
980
1481
|
recoveredTurn = {
|
|
981
1482
|
batchId: `channel-recovered-${crypto.randomUUID()}`,
|
|
982
|
-
|
|
1483
|
+
routingSources: uniqueRoutedSources(sources),
|
|
1484
|
+
lifecycleSources: uniqueLifecycleSources(sources),
|
|
983
1485
|
progress: createChannelTurnProgressBuilder(),
|
|
984
1486
|
richDraft: null,
|
|
985
1487
|
idempotencyScope: createMessageChannelIdempotencyScope()
|
|
@@ -1012,6 +1514,38 @@ class ChannelGateway {
|
|
|
1012
1514
|
});
|
|
1013
1515
|
});
|
|
1014
1516
|
}
|
|
1517
|
+
async publishRuntimeTools(runtime, sources = []) {
|
|
1518
|
+
return await this.enqueueRegistration(async () => {
|
|
1519
|
+
if (this.states.has(runtimeKey(runtime)))
|
|
1520
|
+
return false;
|
|
1521
|
+
const tool = await this.hooks.buildExternalTool(runtime, sources);
|
|
1522
|
+
const response = await this.client.runtimeExternalToolsUpdate({
|
|
1523
|
+
updates: [
|
|
1524
|
+
{
|
|
1525
|
+
runtimes: [runtime],
|
|
1526
|
+
external_tools: tool ? [{ tools: [tool] }] : []
|
|
1527
|
+
}
|
|
1528
|
+
]
|
|
1529
|
+
});
|
|
1530
|
+
if (!response.success) {
|
|
1531
|
+
throw new Error(response.error ?? "Failed to publish channel runtime tools");
|
|
1532
|
+
}
|
|
1533
|
+
return tool !== null;
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
async releaseRuntimeTools(runtime, routedSources = []) {
|
|
1537
|
+
await this.enqueueRegistration(async () => {
|
|
1538
|
+
if (routedSources.length > 0 || this.states.has(runtimeKey(runtime))) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const response = await this.client.runtimeExternalToolsUpdate({
|
|
1542
|
+
updates: [{ runtimes: [runtime], external_tools: [] }]
|
|
1543
|
+
});
|
|
1544
|
+
if (!response.success) {
|
|
1545
|
+
throw new Error(response.error ?? "Failed to release channel runtime tools");
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1015
1549
|
async submitApprovalResponse(runtime, response) {
|
|
1016
1550
|
const result = await this.client.submitInput({
|
|
1017
1551
|
runtime,
|
|
@@ -1020,7 +1554,7 @@ class ChannelGateway {
|
|
|
1020
1554
|
return result.accepted;
|
|
1021
1555
|
}
|
|
1022
1556
|
setRoutedSources(runtime, sources) {
|
|
1023
|
-
this.getState(runtime).routedSources =
|
|
1557
|
+
this.getState(runtime).routedSources = uniqueRoutedSources(sources);
|
|
1024
1558
|
}
|
|
1025
1559
|
getKnownRuntimes() {
|
|
1026
1560
|
return [...this.states.values()].map((state) => state.runtime);
|
|
@@ -1057,7 +1591,6 @@ class ChannelGateway {
|
|
|
1057
1591
|
state = {
|
|
1058
1592
|
runtime,
|
|
1059
1593
|
pendingSourcesByClientMessageId: new Map,
|
|
1060
|
-
queueRevision: 0,
|
|
1061
1594
|
active: null,
|
|
1062
1595
|
registrationSignature: null,
|
|
1063
1596
|
registration: null,
|
|
@@ -1174,43 +1707,81 @@ class ChannelGateway {
|
|
|
1174
1707
|
}
|
|
1175
1708
|
handleQueueUpdate(message) {
|
|
1176
1709
|
const state = this.getState(message.runtime);
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1710
|
+
for (const transition of message.removed) {
|
|
1711
|
+
const pending = state.pendingSourcesByClientMessageId.get(transition.client_message_id);
|
|
1712
|
+
if (pending) {
|
|
1713
|
+
pending.removalDisposition = transition.disposition;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
this.reconcileExplicitQueueRemovals(state);
|
|
1717
|
+
}
|
|
1718
|
+
reconcileExplicitQueueRemovals(state) {
|
|
1719
|
+
const dequeued = [];
|
|
1720
|
+
const cancelled = [];
|
|
1180
1721
|
for (const [
|
|
1181
1722
|
clientMessageId,
|
|
1182
1723
|
pending
|
|
1183
1724
|
] of state.pendingSourcesByClientMessageId) {
|
|
1184
|
-
if (pending.disposition
|
|
1185
|
-
|
|
1186
|
-
state.pendingSourcesByClientMessageId.delete(clientMessageId);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
if (!state.active && removed.length > 0) {
|
|
1190
|
-
const first = removed[0];
|
|
1191
|
-
if (first) {
|
|
1192
|
-
this.activateSources(state, first.clientMessageId, removed.flatMap((entry) => entry.sources));
|
|
1725
|
+
if (pending.disposition !== "queued" || !pending.removalDisposition) {
|
|
1726
|
+
continue;
|
|
1193
1727
|
}
|
|
1728
|
+
const target = pending.removalDisposition === "dequeued" ? dequeued : cancelled;
|
|
1729
|
+
target.push({ clientMessageId, sources: pending.sources });
|
|
1730
|
+
state.pendingSourcesByClientMessageId.delete(clientMessageId);
|
|
1731
|
+
}
|
|
1732
|
+
const firstDequeued = dequeued[0];
|
|
1733
|
+
if (firstDequeued) {
|
|
1734
|
+
this.activateSources(state, firstDequeued.clientMessageId, dequeued.flatMap((entry) => entry.sources));
|
|
1735
|
+
}
|
|
1736
|
+
for (const entry of cancelled) {
|
|
1737
|
+
this.enqueueHook(state, () => this.hooks.onLifecycle({
|
|
1738
|
+
type: "finished",
|
|
1739
|
+
batchId: `channel-${entry.clientMessageId}`,
|
|
1740
|
+
sources: entry.sources,
|
|
1741
|
+
outcome: "cancelled",
|
|
1742
|
+
stopReason: "cancelled"
|
|
1743
|
+
}));
|
|
1194
1744
|
}
|
|
1195
1745
|
}
|
|
1196
1746
|
activateSources(state, clientMessageId, sources) {
|
|
1197
1747
|
if (state.active) {
|
|
1748
|
+
const knownLifecycleKeys = new Set(state.active.lifecycleSources.map(sourceLifecycleKey));
|
|
1749
|
+
const addedLifecycleSources = uniqueLifecycleSources(sources).filter((source) => !knownLifecycleKeys.has(sourceLifecycleKey(source)));
|
|
1750
|
+
if (addedLifecycleSources.length === 0)
|
|
1751
|
+
return;
|
|
1752
|
+
state.active.lifecycleSources = uniqueLifecycleSources([
|
|
1753
|
+
...state.active.lifecycleSources,
|
|
1754
|
+
...addedLifecycleSources
|
|
1755
|
+
]);
|
|
1756
|
+
state.active.routingSources = uniqueRoutedSources([
|
|
1757
|
+
...state.active.routingSources,
|
|
1758
|
+
...sources
|
|
1759
|
+
]);
|
|
1760
|
+
const processingEvent2 = {
|
|
1761
|
+
type: "processing",
|
|
1762
|
+
batchId: state.active.batchId,
|
|
1763
|
+
sources: addedLifecycleSources
|
|
1764
|
+
};
|
|
1765
|
+
this.enqueueHook(state, () => this.hooks.onLifecycle(processingEvent2));
|
|
1198
1766
|
return;
|
|
1199
1767
|
}
|
|
1768
|
+
const routingSources = uniqueRoutedSources(sources);
|
|
1769
|
+
const lifecycleSources = uniqueLifecycleSources(sources);
|
|
1200
1770
|
state.active = {
|
|
1201
1771
|
batchId: `channel-${clientMessageId}`,
|
|
1202
|
-
|
|
1772
|
+
routingSources,
|
|
1773
|
+
lifecycleSources,
|
|
1203
1774
|
progress: createChannelTurnProgressBuilder(),
|
|
1204
1775
|
richDraft: this.hooks.createRichDraft?.({
|
|
1205
1776
|
batchId: `channel-${clientMessageId}`,
|
|
1206
|
-
sources
|
|
1777
|
+
sources: routingSources
|
|
1207
1778
|
}) ?? null,
|
|
1208
1779
|
idempotencyScope: createMessageChannelIdempotencyScope()
|
|
1209
1780
|
};
|
|
1210
1781
|
const processingEvent = {
|
|
1211
1782
|
type: "processing",
|
|
1212
1783
|
batchId: state.active.batchId,
|
|
1213
|
-
sources: state.active.
|
|
1784
|
+
sources: state.active.lifecycleSources
|
|
1214
1785
|
};
|
|
1215
1786
|
this.enqueueHook(state, () => this.hooks.onLifecycle(processingEvent));
|
|
1216
1787
|
}
|
|
@@ -1228,7 +1799,7 @@ class ChannelGateway {
|
|
|
1228
1799
|
this.enqueueHook(state, () => this.hooks.onProgress({
|
|
1229
1800
|
type: "progress",
|
|
1230
1801
|
batchId: active.batchId,
|
|
1231
|
-
sources: active.
|
|
1802
|
+
sources: active.routingSources,
|
|
1232
1803
|
...update
|
|
1233
1804
|
}));
|
|
1234
1805
|
}
|
|
@@ -1248,7 +1819,7 @@ class ChannelGateway {
|
|
|
1248
1819
|
this.enqueueHook(state, () => this.hooks.onLifecycle({
|
|
1249
1820
|
type: "finished",
|
|
1250
1821
|
batchId: active.batchId,
|
|
1251
|
-
sources: active.
|
|
1822
|
+
sources: active.lifecycleSources,
|
|
1252
1823
|
outcome: lifecycleOutcome(terminal.stopReason),
|
|
1253
1824
|
stopReason: terminal.stopReason,
|
|
1254
1825
|
...terminal.runId ?? active.runId ? { runId: terminal.runId ?? active.runId } : {},
|
|
@@ -1264,9 +1835,9 @@ class ChannelGateway {
|
|
|
1264
1835
|
}));
|
|
1265
1836
|
if (!state)
|
|
1266
1837
|
return;
|
|
1267
|
-
const sources = state.active?.
|
|
1838
|
+
const sources = state.active?.routingSources ?? [];
|
|
1268
1839
|
state.replayedControlRequestIds.add(message.request_id);
|
|
1269
|
-
const sourceScopes = new Map(sources.map((source2) => [
|
|
1840
|
+
const sourceScopes = new Map(sources.map((source2) => [sourceRouteKey(source2), source2]));
|
|
1270
1841
|
if (sourceScopes.size !== 1)
|
|
1271
1842
|
return;
|
|
1272
1843
|
const source = [...sourceScopes.values()][0];
|
|
@@ -2312,10 +2883,12 @@ function buildMessageChannelExternalToolDefinition(options) {
|
|
|
2312
2883
|
};
|
|
2313
2884
|
}
|
|
2314
2885
|
export {
|
|
2886
|
+
formatChannelControlRequestPrompt,
|
|
2315
2887
|
executeMessageChannelExternalTool,
|
|
2316
2888
|
executeMessageChannel,
|
|
2317
2889
|
buildMessageChannelExternalToolDefinition,
|
|
2318
|
-
ChannelGateway
|
|
2890
|
+
ChannelGateway,
|
|
2891
|
+
ChannelControlRequestCoordinator
|
|
2319
2892
|
};
|
|
2320
2893
|
|
|
2321
|
-
//# debugId=
|
|
2894
|
+
//# debugId=CA56D3159193925764756E2164756E21
|