@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.
Files changed (46) hide show
  1. package/dist/gateway-core.js +604 -31
  2. package/dist/gateway-core.js.map +6 -4
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/types/agent/model.d.ts.map +1 -1
  6. package/dist/types/agent/subagents/subagent-launcher.d.ts.map +1 -1
  7. package/dist/types/channels/control-request-coordinator.d.ts +42 -0
  8. package/dist/types/channels/control-request-coordinator.d.ts.map +1 -0
  9. package/dist/types/channels/gateway-core.d.ts +4 -0
  10. package/dist/types/channels/gateway-core.d.ts.map +1 -1
  11. package/dist/types/channels/interactive.d.ts +13 -0
  12. package/dist/types/channels/interactive.d.ts.map +1 -0
  13. package/dist/types/gateway-core.d.ts +3 -0
  14. package/dist/types/gateway-core.d.ts.map +1 -1
  15. package/dist/types/mods/capabilities.d.ts +4 -0
  16. package/dist/types/mods/capabilities.d.ts.map +1 -1
  17. package/dist/types/mods/mod-adapter.d.ts.map +1 -1
  18. package/dist/types/tools/impl/bash.d.ts.map +1 -1
  19. package/dist/types/tools/impl/foreground-sleep.d.ts +13 -0
  20. package/dist/types/tools/impl/foreground-sleep.d.ts.map +1 -0
  21. package/dist/types/tools/impl/task-update.d.ts +1 -1
  22. package/dist/types/tools/impl/task-update.d.ts.map +1 -1
  23. package/dist/types/tools/impl/task.d.ts +1 -0
  24. package/dist/types/tools/impl/task.d.ts.map +1 -1
  25. package/dist/types/tools/impl/tasks/store.d.ts +3 -6
  26. package/dist/types/tools/impl/tasks/store.d.ts.map +1 -1
  27. package/dist/types/types/app-server-protocol.d.ts +1 -0
  28. package/dist/types/types/app-server-protocol.d.ts.map +1 -1
  29. package/dist/types/types/protocol_v2.d.ts +11 -13
  30. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  31. package/dist/types/types/queue-update-protocol.d.ts +5 -0
  32. package/dist/types/types/queue-update-protocol.d.ts.map +1 -0
  33. package/dist/types/types/service-protocol.d.ts +11 -0
  34. package/dist/types/types/service-protocol.d.ts.map +1 -1
  35. package/dist/types/types/teleport-protocol.d.ts +54 -0
  36. package/dist/types/types/teleport-protocol.d.ts.map +1 -0
  37. package/dist/types/websocket/listener/protocol-outbound.d.ts +3 -6
  38. package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
  39. package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
  40. package/dist/types/websocket/listener/types.d.ts +13 -1
  41. package/dist/types/websocket/listener/types.d.ts.map +1 -1
  42. package/letta.js +2572 -1275
  43. package/package.json +1 -1
  44. package/scripts/source-file-size-baseline.json +5 -5
  45. package/skills/scheduling-tasks/SKILL.md +10 -8
  46. package/skills/teleporting-between-environments/SKILL.md +100 -0
@@ -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 sourceKey(source) {
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 uniqueSources(sources) {
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(sourceKey(source), source);
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?.sources ?? state?.routedSources ?? [];
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: uniqueSources(delivery.sources),
1423
+ sources: uniqueLifecycleSources(delivery.sources),
923
1424
  disposition: "submitting"
924
1425
  });
925
1426
  try {
926
1427
  await this.enqueueRegistration(async () => {
927
- state.routedSources = uniqueSources([
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
- sources: uniqueSources(sources),
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 = uniqueSources(sources);
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
- state.queueRevision += 1;
1178
- const nextQueued = new Set(message.queue.map((entry) => entry.client_message_id));
1179
- const removed = [];
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 === "queued" && state.queueRevision > (pending.acceptedAtQueueRevision ?? -1) && !nextQueued.has(clientMessageId)) {
1185
- removed.push({ clientMessageId, sources: pending.sources });
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
- sources: uniqueSources(sources),
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.sources
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.sources,
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.sources,
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?.sources ?? [];
1838
+ const sources = state.active?.routingSources ?? [];
1268
1839
  state.replayedControlRequestIds.add(message.request_id);
1269
- const sourceScopes = new Map(sources.map((source2) => [sourceKey(source2), 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=E1A24CF28F090AA064756E2164756E21
2894
+ //# debugId=CA56D3159193925764756E2164756E21