@letta-ai/letta-code 0.30.10 → 0.30.11
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/channels-public.js +411 -2
- package/dist/channels-public.js.map +6 -3
- package/dist/channels-slack.js +209 -11
- package/dist/channels-slack.js.map +6 -5
- package/dist/gateway-core.js +110 -13
- package/dist/gateway-core.js.map +6 -5
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/schedules.js +4 -2
- package/dist/schedules.js.map +3 -3
- package/dist/types/agent/client-skills.d.ts +2 -0
- package/dist/types/agent/client-skills.d.ts.map +1 -1
- package/dist/types/agent/message.d.ts.map +1 -1
- package/dist/types/backend/local/local-store.d.ts.map +1 -1
- package/dist/types/channels/command-surface.d.ts +54 -0
- package/dist/types/channels/command-surface.d.ts.map +1 -0
- package/dist/types/channels/gateway-core.d.ts +2 -1
- package/dist/types/channels/gateway-core.d.ts.map +1 -1
- package/dist/types/channels/message-channel-executor.d.ts +2 -0
- package/dist/types/channels/message-channel-executor.d.ts.map +1 -1
- package/dist/types/channels/message-channel-idempotency.d.ts +13 -0
- package/dist/types/channels/message-channel-idempotency.d.ts.map +1 -0
- package/dist/types/channels-public.d.ts +4 -0
- package/dist/types/channels-public.d.ts.map +1 -1
- package/dist/types/channels-slack.d.ts +1 -0
- package/dist/types/channels-slack.d.ts.map +1 -1
- package/dist/types/cron/scheduled-task-prompt.d.ts +7 -0
- package/dist/types/cron/scheduled-task-prompt.d.ts.map +1 -1
- package/dist/types/schedules.d.ts +1 -1
- package/dist/types/schedules.d.ts.map +1 -1
- package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
- package/dist/types/websocket/listener/types.d.ts +2 -1
- package/dist/types/websocket/listener/types.d.ts.map +1 -1
- package/letta.js +388 -226
- package/package.json +1 -1
- package/scripts/source-file-size-baseline.json +1 -1
package/dist/gateway-core.js
CHANGED
|
@@ -14,6 +14,53 @@ function getInteractiveApprovalKind(toolName) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// src/channels/message-channel-idempotency.ts
|
|
18
|
+
class MessageChannelDuplicateActionError extends Error {
|
|
19
|
+
constructor(state) {
|
|
20
|
+
const detail = state === "in-flight" ? "an identical text send is already in progress" : "the immediately previous MessageChannel call already sent this exact text to the same destination";
|
|
21
|
+
super(`Duplicate MessageChannel action suppressed: ${detail}. The duplicate was not sent; continue the turn instead of retrying it.`);
|
|
22
|
+
this.name = "MessageChannelDuplicateActionError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function isErrorResult(result) {
|
|
26
|
+
return result.startsWith("Error:");
|
|
27
|
+
}
|
|
28
|
+
function createMessageChannelIdempotencyScope() {
|
|
29
|
+
const inFlight = new Map;
|
|
30
|
+
let lastSuccessful = null;
|
|
31
|
+
let latestInvocation = 0;
|
|
32
|
+
return {
|
|
33
|
+
async execute(key, effect) {
|
|
34
|
+
if (!key) {
|
|
35
|
+
latestInvocation += 1;
|
|
36
|
+
lastSuccessful = null;
|
|
37
|
+
return await effect();
|
|
38
|
+
}
|
|
39
|
+
const pendingDuplicate = inFlight.get(key);
|
|
40
|
+
if (pendingDuplicate) {
|
|
41
|
+
throw new MessageChannelDuplicateActionError("in-flight");
|
|
42
|
+
}
|
|
43
|
+
if (lastSuccessful?.key === key) {
|
|
44
|
+
throw new MessageChannelDuplicateActionError("completed");
|
|
45
|
+
}
|
|
46
|
+
const invocation = ++latestInvocation;
|
|
47
|
+
lastSuccessful = null;
|
|
48
|
+
const pending = Promise.resolve().then(effect);
|
|
49
|
+
inFlight.set(key, pending);
|
|
50
|
+
try {
|
|
51
|
+
const result = await pending;
|
|
52
|
+
if (invocation === latestInvocation && !isErrorResult(result)) {
|
|
53
|
+
lastSuccessful = { key };
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
} finally {
|
|
57
|
+
if (inFlight.get(key) === pending)
|
|
58
|
+
inFlight.delete(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
17
64
|
// src/channels/progress-formatting.ts
|
|
18
65
|
var MAX_PROGRESS_TEXT_LENGTH = 140;
|
|
19
66
|
var MAX_PROGRESS_DETAILS_LENGTH = 180;
|
|
@@ -844,8 +891,9 @@ class ChannelGateway {
|
|
|
844
891
|
this.hooks = hooks;
|
|
845
892
|
this.disposers.push(client.onMessage((message) => this.handleMessage(message)), client.onExternalToolCall((request) => {
|
|
846
893
|
const state = request.runtime ? this.states.get(runtimeKey(request.runtime)) : undefined;
|
|
847
|
-
const
|
|
848
|
-
|
|
894
|
+
const active = state?.active;
|
|
895
|
+
const sources = active?.sources ?? state?.routedSources ?? [];
|
|
896
|
+
return hooks.executeExternalTool(request, sources, active?.idempotencyScope ?? null);
|
|
849
897
|
}));
|
|
850
898
|
}
|
|
851
899
|
close() {
|
|
@@ -933,7 +981,8 @@ class ChannelGateway {
|
|
|
933
981
|
batchId: `channel-recovered-${crypto.randomUUID()}`,
|
|
934
982
|
sources: uniqueSources(sources),
|
|
935
983
|
progress: createChannelTurnProgressBuilder(),
|
|
936
|
-
richDraft: null
|
|
984
|
+
richDraft: null,
|
|
985
|
+
idempotencyScope: createMessageChannelIdempotencyScope()
|
|
937
986
|
};
|
|
938
987
|
state.active = recoveredTurn;
|
|
939
988
|
}
|
|
@@ -1155,7 +1204,8 @@ class ChannelGateway {
|
|
|
1155
1204
|
richDraft: this.hooks.createRichDraft?.({
|
|
1156
1205
|
batchId: `channel-${clientMessageId}`,
|
|
1157
1206
|
sources
|
|
1158
|
-
}) ?? null
|
|
1207
|
+
}) ?? null,
|
|
1208
|
+
idempotencyScope: createMessageChannelIdempotencyScope()
|
|
1159
1209
|
};
|
|
1160
1210
|
const processingEvent = {
|
|
1161
1211
|
type: "processing",
|
|
@@ -1897,6 +1947,50 @@ async function dispatchMessageChannelAction(params) {
|
|
|
1897
1947
|
formatText: (text) => formatOutboundChannelMessage(params.request.channel, text)
|
|
1898
1948
|
});
|
|
1899
1949
|
}
|
|
1950
|
+
function trimmedOrNull(value) {
|
|
1951
|
+
const trimmed = value?.trim();
|
|
1952
|
+
return trimmed ? trimmed : null;
|
|
1953
|
+
}
|
|
1954
|
+
function effectiveTextThreadId(request, route) {
|
|
1955
|
+
const requestThreadId = trimmedOrNull(request.threadId);
|
|
1956
|
+
const routeThreadId = trimmedOrNull(route.threadId);
|
|
1957
|
+
if (request.channel === "telegram") {
|
|
1958
|
+
if (requestThreadId)
|
|
1959
|
+
return requestThreadId;
|
|
1960
|
+
if (route.chatType === "direct")
|
|
1961
|
+
return null;
|
|
1962
|
+
return route.chatId.trim().startsWith("-") ? routeThreadId : null;
|
|
1963
|
+
}
|
|
1964
|
+
if (request.channel === "discord") {
|
|
1965
|
+
return route.chatType === "direct" ? route.chatId : requestThreadId ?? routeThreadId;
|
|
1966
|
+
}
|
|
1967
|
+
if (request.channel === "slack") {
|
|
1968
|
+
const isDirect = route.chatType === "direct" || request.chatId.startsWith("D");
|
|
1969
|
+
if (isDirect)
|
|
1970
|
+
return requestThreadId ?? routeThreadId;
|
|
1971
|
+
return request.replyToMessageId ? null : requestThreadId ?? routeThreadId;
|
|
1972
|
+
}
|
|
1973
|
+
return null;
|
|
1974
|
+
}
|
|
1975
|
+
function effectiveTextReplyId(request, route) {
|
|
1976
|
+
const isSlackDirect = request.channel === "slack" && (route.chatType === "direct" || request.chatId.startsWith("D"));
|
|
1977
|
+
return isSlackDirect ? null : trimmedOrNull(request.replyToMessageId);
|
|
1978
|
+
}
|
|
1979
|
+
function messageIdempotencyKey(request, route) {
|
|
1980
|
+
if (request.action !== "send" && request.action !== "send-rich" || request.mediaPath) {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
return JSON.stringify({
|
|
1984
|
+
action: request.action,
|
|
1985
|
+
channel: request.channel,
|
|
1986
|
+
chatId: route.chatId,
|
|
1987
|
+
accountId: route.accountId ?? null,
|
|
1988
|
+
chatType: route.chatType ?? null,
|
|
1989
|
+
threadId: effectiveTextThreadId(request, route),
|
|
1990
|
+
message: request.message ?? null,
|
|
1991
|
+
replyToMessageId: effectiveTextReplyId(request, route)
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1900
1994
|
async function executeMessageChannel(input, options) {
|
|
1901
1995
|
const normalized = normalizeMessageChannelInput(input, options.resolver);
|
|
1902
1996
|
if (typeof normalized === "string")
|
|
@@ -1931,10 +2025,8 @@ async function executeMessageChannel(input, options) {
|
|
|
1931
2025
|
channelTurnSources: options.channelTurnSources
|
|
1932
2026
|
});
|
|
1933
2027
|
const requestThreadId = normalized.action === "download-file" ? normalized.threadId : inferredThreadId ?? (normalized.channel === "telegram" && context2.route.chatType === "direct" ? normalized.threadId : context2.route.threadId ?? normalized.threadId);
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
context: context2
|
|
1937
|
-
});
|
|
2028
|
+
const request2 = buildMessageChannelRequest(normalized, normalized.chatId, requestThreadId);
|
|
2029
|
+
return await dispatchWithIdempotency(request2, context2, options.idempotencyScope);
|
|
1938
2030
|
}
|
|
1939
2031
|
if (normalized.channel !== "slack") {
|
|
1940
2032
|
return `Error: Explicit MessageChannel targets are not supported on ${normalized.channel}.`;
|
|
@@ -1955,15 +2047,20 @@ async function executeMessageChannel(input, options) {
|
|
|
1955
2047
|
transport: proactive.transport,
|
|
1956
2048
|
messageActions: proactive.messageActions
|
|
1957
2049
|
};
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
context
|
|
1961
|
-
});
|
|
2050
|
+
const request = buildMessageChannelRequest(normalized, proactive.target.chatId, proactive.target.threadId);
|
|
2051
|
+
return await dispatchWithIdempotency(request, context, options.idempotencyScope);
|
|
1962
2052
|
} catch (error) {
|
|
2053
|
+
if (error instanceof MessageChannelDuplicateActionError)
|
|
2054
|
+
throw error;
|
|
1963
2055
|
const message = error instanceof Error ? error.message : "unknown error";
|
|
1964
2056
|
return `Error sending message to ${normalized.channel}: ${message}`;
|
|
1965
2057
|
}
|
|
1966
2058
|
}
|
|
2059
|
+
function dispatchWithIdempotency(request, context, scope) {
|
|
2060
|
+
const dispatch = () => dispatchMessageChannelAction({ request, context });
|
|
2061
|
+
const key = messageIdempotencyKey(request, context.route);
|
|
2062
|
+
return scope ? scope.execute(key, dispatch) : dispatch();
|
|
2063
|
+
}
|
|
1967
2064
|
// src/tools/descriptions/MessageChannel.md
|
|
1968
2065
|
var MessageChannel_default = '# MessageChannel\n\nSend a message or channel action to an external channel.\n\nWhen you receive a `<channel-notification>`, use this tool to reply directly to the user on the same external channel. A normal assistant response is not delivered back to the external channel automatically.\n\nThere are two supported send modes:\n- Reply mode: use `channel` + `chat_id` from the notification to respond in the current routed chat.\n- Proactive mode: use `channel` + `target` on supported channels to send to an explicit outbound destination.\n\nPreferred reply pattern:\n- `action="send"` to send a normal reply\n- `channel` + `chat_id` from the notification attributes\n- `message` for the text body\n\nParameters:\n- `action`: The action to perform. The exact available actions depend on the active channel plugins and are reflected in the JSON schema.\n- `channel`: The platform to send to.\n- `chat_id`: Reply target for the current routed chat. Use this when responding to a channel notification.\n- `target`: Explicit outbound target for proactive sends on supported channels.\n- `accountId`: Optional channel account selector when multiple eligible accounts are available.\n- `message`: Text body for `action="send"`.\n- `replyTo`: Optional message ID to reply to. Omit this unless you intentionally want the platform\'s quote/reply UI.\n- `messageId`: Optional target message id for message-scoped actions like reactions.\n- `emoji`: Optional reaction payload for channels that support reactions.\n- `remove`: Optional boolean to remove a reaction instead of adding it.\n- `media`: Optional absolute local file path for file/media uploads on channels that support uploads.\n- `filename`: Optional uploaded filename override when supported by the channel.\n- `title`: Optional uploaded attachment title when supported by the channel.\n\nRules:\n- Always pass `action` explicitly, even for a normal reply.\n- Pass exactly one of `chat_id` or `target`.\n- `react` should be its own call.\n- `upload-file` can include both `media` and `message` so the uploaded file has a caption/comment when the channel supports it.\n- Telegram supports `action="send-rich"` for Bot API Rich Messages from Markdown content. Use it for headings, lists, tables, rich block quotes, details blocks, formulas, and longer structured messages; use `upload-file` for local media files.\n\nTelegram rich messages:\n- In Telegram private chats, normal `action="send"` messages are sent through Bot API Rich Messages by default when the Telegram account enables `rich_private_chat_default`; with that setting disabled, use explicit `action="send-rich"` for rich delivery.\n- Use `action="send-rich"` with `channel="telegram"` for structured Markdown rendered by Telegram Bot API Rich Messages.\n- Use this when the output benefits from real headings, tables, block quotes, collapsible details, footnotes, task lists, or formulas.\n- The `message` field is passed as Telegram rich Markdown. Supported examples include:\n - headings: `# Heading`, `## Heading`\n - lists: `- item`, `1. item`, `- [ ] task`, `- [x] done`\n - tables: GitHub-style pipe tables\n - block quotes: `> quoted text`\n - collapsible details: `<details><summary>Title</summary>content</details>`\n - inline math: `$E = mc^2$`\n - display math: `$$E = mc^2$$` or fenced `math` code blocks\n - footnotes: `text[^id]` with `[^id]: definition`\n- Prefer dollar-delimited math. `\\(...\\)` and `\\[...\\]` do not reliably render as formulas in Telegram rich Markdown.\n- Do not use `send-rich` for local file uploads. Use `upload-file`; rich Markdown media blocks only support HTTP/HTTPS URLs.\n- Rich messages persist only when the final `send-rich` call executes. If Telegram draft streaming is enabled for the account, the channel runtime may show ephemeral previews automatically; do not try to manage draft lifecycle from the tool call.\n';
|
|
1969
2066
|
// src/tools/schemas/MessageChannel.json
|
|
@@ -2211,4 +2308,4 @@ export {
|
|
|
2211
2308
|
ChannelGateway
|
|
2212
2309
|
};
|
|
2213
2310
|
|
|
2214
|
-
//# debugId=
|
|
2311
|
+
//# debugId=DFB30BEB2B5EB10F64756E2164756E21
|