@geminixiang/mikan 1.0.0-beta.40 → 1.0.0-beta.41
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/CHANGELOG.md +26 -0
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +7 -6
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/components.d.ts +33 -0
- package/dist/adapters/discord/components.d.ts.map +1 -0
- package/dist/adapters/discord/components.js +32 -0
- package/dist/adapters/discord/components.js.map +1 -0
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +16 -6
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/discord/format.d.ts +3 -0
- package/dist/adapters/discord/format.d.ts.map +1 -0
- package/dist/adapters/discord/format.js +86 -0
- package/dist/adapters/discord/format.js.map +1 -0
- package/dist/adapters/markdown-tables.d.ts +37 -0
- package/dist/adapters/markdown-tables.d.ts.map +1 -0
- package/dist/adapters/markdown-tables.js +164 -0
- package/dist/adapters/markdown-tables.js.map +1 -0
- package/dist/adapters/progressive-renderer.d.ts.map +1 -1
- package/dist/adapters/progressive-renderer.js +69 -12
- package/dist/adapters/progressive-renderer.js.map +1 -1
- package/dist/adapters/slack/assistant.d.ts +101 -0
- package/dist/adapters/slack/assistant.d.ts.map +1 -0
- package/dist/adapters/slack/assistant.js +204 -0
- package/dist/adapters/slack/assistant.js.map +1 -0
- package/dist/adapters/slack/blocks.d.ts.map +1 -1
- package/dist/adapters/slack/blocks.js +4 -82
- package/dist/adapters/slack/blocks.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +52 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +161 -5
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
- package/dist/adapters/slack/response-lifecycle.js +86 -15
- package/dist/adapters/slack/response-lifecycle.js.map +1 -1
- package/dist/adapters/slack/stream-limits.d.ts +35 -0
- package/dist/adapters/slack/stream-limits.d.ts.map +1 -0
- package/dist/adapters/slack/stream-limits.js +56 -0
- package/dist/adapters/slack/stream-limits.js.map +1 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +26 -47
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +7 -19
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/types.d.ts +24 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -1
- package/dist/agent.js.map +1 -1
- package/dist/commands/extensions.d.ts +18 -6
- package/dist/commands/extensions.d.ts.map +1 -1
- package/dist/commands/extensions.js +55 -8
- package/dist/commands/extensions.js.map +1 -1
- package/dist/env-manifest.d.ts.map +1 -1
- package/dist/env-manifest.js +4 -0
- package/dist/env-manifest.js.map +1 -1
- package/dist/harness/subagent-runner.d.ts +6 -0
- package/dist/harness/subagent-runner.d.ts.map +1 -1
- package/dist/harness/subagent-runner.js +63 -0
- package/dist/harness/subagent-runner.js.map +1 -1
- package/dist/subagent-progress.d.ts +3 -7
- package/dist/subagent-progress.d.ts.map +1 -1
- package/dist/subagent-progress.js +15 -10
- package/dist/subagent-progress.js.map +1 -1
- package/dist/tools/subagent.d.ts +8 -1
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +24 -1
- package/dist/tools/subagent.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web/admin/portal.js +5 -1
- package/dist/web/admin/portal.js.map +1 -1
- package/package.json +2 -1
- package/dist/adapters/telegram/html.d.ts +0 -3
- package/dist/adapters/telegram/html.d.ts.map +0 -1
- package/dist/adapters/telegram/html.js +0 -101
- package/dist/adapters/telegram/html.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SocketModeClient } from "@slack/socket-mode";
|
|
2
2
|
import { WebAPIRateLimitedError, WebClient } from "@slack/web-api";
|
|
3
|
+
import { readEnv } from "../../env-manifest.js";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
5
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
6
|
import { basename, join } from "node:path";
|
|
@@ -13,13 +14,31 @@ import { isRecord, readTextFileIfExists } from "../../utils/file-guards.js";
|
|
|
13
14
|
import { PRODUCT_NAME, formatForceStopped } from "../../platform-messages.js";
|
|
14
15
|
import { appendBotResponseLog, appendChannelLog, MessagingEventQueue, saveIncomingAttachments, withRetry, } from "../shared.js";
|
|
15
16
|
import { processMessageIntake } from "../intake.js";
|
|
17
|
+
import { AssistantThreadRegistry, handleAgentContextChanged, handleAgentDmOpened, handleAssistantThreadStarted, titleAssistantThread, } from "./assistant.js";
|
|
16
18
|
import { createSlackAdapters } from "./context.js";
|
|
17
19
|
import { hasMaterializedChatSession, registerThreadSession, } from "../../sessions/chat-history-sync.js";
|
|
18
20
|
import { conversationIdOf } from "../../sessions/session-key.js";
|
|
19
21
|
import { isSlackThreadSessionKey, planSlackAdapterSession, planSlackEventAnchorRun, resolveSlackSessionKey, } from "./session.js";
|
|
20
22
|
import { reportUserFacingError } from "../../observability/sentry.js";
|
|
21
23
|
import { renderSlackBlocks, resolveSlackMentions } from "./blocks.js";
|
|
24
|
+
import { StreamStartLimiter } from "./stream-limits.js";
|
|
22
25
|
const SLACK_EVENT_ANCHOR_TEXT = "Working on it...";
|
|
26
|
+
/**
|
|
27
|
+
* Slack's per-call limit on `markdown_text`: "Limit this field to 12,000
|
|
28
|
+
* characters." mikan allows far more in a message, so streamed text is split
|
|
29
|
+
* to fit rather than truncated.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_STREAM_TEXT_CHARS = 12_000;
|
|
32
|
+
/** Slices `text` into pieces that each fit one streaming call. */
|
|
33
|
+
export function chunkStreamText(text, limit = MAX_STREAM_TEXT_CHARS) {
|
|
34
|
+
if (text.length <= limit)
|
|
35
|
+
return [text];
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for (let index = 0; index < text.length; index += limit) {
|
|
38
|
+
chunks.push(text.slice(index, index + limit));
|
|
39
|
+
}
|
|
40
|
+
return chunks;
|
|
41
|
+
}
|
|
23
42
|
// web-api v8 throws WebAPIRateLimitedError; the duck-typed shapes cover
|
|
24
43
|
// platform `data.error === "rate_limited"` / 429 responses and older callers.
|
|
25
44
|
function slackIsRateLimited(err) {
|
|
@@ -112,6 +131,15 @@ export class SlackMessagingBot {
|
|
|
112
131
|
this.startupTs = null; // Messages older than this are just logged, not processed
|
|
113
132
|
this.users = new Map();
|
|
114
133
|
this.channels = new Map();
|
|
134
|
+
/** Which threads belong to the assistant pane, and their last known context. */
|
|
135
|
+
this.assistantThreads = new AssistantThreadRegistry();
|
|
136
|
+
/**
|
|
137
|
+
* Native response streams opened recently. Per bot rather than per module:
|
|
138
|
+
* the rate tier applies per workspace, and this bot is the connection to
|
|
139
|
+
* one — which also means a test constructing a bot gets a fresh budget
|
|
140
|
+
* instead of inheriting whatever ran before it.
|
|
141
|
+
*/
|
|
142
|
+
this.streamStarts = new StreamStartLimiter();
|
|
115
143
|
this.queues = new Map();
|
|
116
144
|
this.eventsWatcher = null;
|
|
117
145
|
this.handler = handler;
|
|
@@ -319,22 +347,54 @@ export class SlackMessagingBot {
|
|
|
319
347
|
await this.webClient.chat.update(payload);
|
|
320
348
|
});
|
|
321
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Claim one native stream, or decline. Declining is not an error: the caller
|
|
352
|
+
* falls back to edit-based updates, which is a working answer rather than a
|
|
353
|
+
* degraded one, and costs no request against the tighter start tier.
|
|
354
|
+
*/
|
|
355
|
+
tryReserveStreamStart() {
|
|
356
|
+
return this.streamStarts.tryReserve();
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Open a stream, carrying text of any length.
|
|
360
|
+
*
|
|
361
|
+
* Slack caps `markdown_text` at 12,000 characters per call, and a long first
|
|
362
|
+
* render exceeds that easily — mikan allows 35,000 in a message. Rather than
|
|
363
|
+
* truncating (losing the answer) or failing (falling back to edit mode for
|
|
364
|
+
* the whole response), the opening call takes the first slice and the rest
|
|
365
|
+
* follows as appends before this resolves. The caller sees one operation
|
|
366
|
+
* that sent everything, so the renderer's idea of what has been streamed
|
|
367
|
+
* stays true.
|
|
368
|
+
*
|
|
369
|
+
* Splitting mid-word or mid-fence is harmless here: a stream concatenates,
|
|
370
|
+
* so Slack renders the reassembled text, not the pieces.
|
|
371
|
+
*/
|
|
322
372
|
async startMessageStream(channel, text, threadTs, recipientUserId) {
|
|
323
|
-
|
|
373
|
+
const [head, ...tail] = chunkStreamText(text);
|
|
374
|
+
const ts = await slackRetry(async () => {
|
|
324
375
|
const result = await this.webClient.apiCall("chat.startStream", {
|
|
325
376
|
channel,
|
|
326
|
-
markdown_text: this.resolveMentions(
|
|
377
|
+
markdown_text: this.resolveMentions(head ?? ""),
|
|
327
378
|
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
328
379
|
...(this.teamId ? { recipient_team_id: this.teamId } : {}),
|
|
329
380
|
...(recipientUserId ? { recipient_user_id: recipientUserId } : {}),
|
|
330
381
|
});
|
|
331
|
-
const
|
|
332
|
-
if (!
|
|
382
|
+
const streamTs = result.ts;
|
|
383
|
+
if (!streamTs)
|
|
333
384
|
throw new Error("Slack chat.startStream did not return ts");
|
|
334
|
-
return
|
|
385
|
+
return streamTs;
|
|
335
386
|
});
|
|
387
|
+
for (const part of tail)
|
|
388
|
+
await this.appendStreamChunk(channel, ts, part);
|
|
389
|
+
return ts;
|
|
336
390
|
}
|
|
391
|
+
/** Append a delta of any length, split to the per-call cap. */
|
|
337
392
|
async appendMessageStream(channel, ts, text) {
|
|
393
|
+
for (const part of chunkStreamText(text)) {
|
|
394
|
+
await this.appendStreamChunk(channel, ts, part);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async appendStreamChunk(channel, ts, text) {
|
|
338
398
|
return slackRetry(async () => {
|
|
339
399
|
await this.webClient.apiCall("chat.appendStream", {
|
|
340
400
|
channel,
|
|
@@ -368,6 +428,69 @@ export class SlackMessagingBot {
|
|
|
368
428
|
});
|
|
369
429
|
});
|
|
370
430
|
}
|
|
431
|
+
/** Offer starting points in a new assistant thread. */
|
|
432
|
+
async setAssistantSuggestedPrompts(channel, threadTs, prompts) {
|
|
433
|
+
return slackRetry(async () => {
|
|
434
|
+
// Omitting thread_ts pins the prompts to the DM itself, which is what
|
|
435
|
+
// the agent surface wants: there is no thread yet when it opens.
|
|
436
|
+
await this.webClient.assistant.threads.setSuggestedPrompts({
|
|
437
|
+
channel_id: channel,
|
|
438
|
+
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
439
|
+
prompts,
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/** Name an assistant thread, which is what the sidebar conversation list shows. */
|
|
444
|
+
async setAssistantTitle(channel, threadTs, title) {
|
|
445
|
+
return slackRetry(async () => {
|
|
446
|
+
await this.webClient.assistant.threads.setTitle({
|
|
447
|
+
channel_id: channel,
|
|
448
|
+
thread_ts: threadTs,
|
|
449
|
+
title,
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/** The assistant surface's view of this bot, handed to the lifecycle handlers. */
|
|
454
|
+
assistantOps() {
|
|
455
|
+
return {
|
|
456
|
+
postInThread: (channel, threadTs, text) => this.postInThread(channel, threadTs, text),
|
|
457
|
+
setSuggestedPrompts: (channel, threadTs, prompts) => this.setAssistantSuggestedPrompts(channel, threadTs, prompts),
|
|
458
|
+
setTitle: (channel, threadTs, title) => this.setAssistantTitle(channel, threadTs, title),
|
|
459
|
+
channelName: (channelId) => this.channels.get(channelId)?.name,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* A new conversation opened in the assistant pane. Slack waits for the app
|
|
464
|
+
* to speak first here; staying silent is what makes the pane look broken.
|
|
465
|
+
*/
|
|
466
|
+
handleAssistantThreadStarted({ event, ack }) {
|
|
467
|
+
ack();
|
|
468
|
+
const payload = event;
|
|
469
|
+
if (!payload.assistant_thread)
|
|
470
|
+
return;
|
|
471
|
+
void handleAssistantThreadStarted(this.assistantOps(), this.assistantThreads, payload.assistant_thread);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* The person navigated to another channel while the pane is open. Slack
|
|
475
|
+
* sends this as `assistant_thread_context_changed` under `assistant_view`
|
|
476
|
+
* and `app_context_changed` under `agent_view`; the payload differs enough
|
|
477
|
+
* to normalize here rather than in the handler.
|
|
478
|
+
*/
|
|
479
|
+
handleAgentContextChangedEvent({ event, ack, }) {
|
|
480
|
+
ack();
|
|
481
|
+
const payload = event;
|
|
482
|
+
if (payload.assistant_thread) {
|
|
483
|
+
handleAgentContextChanged(this.assistantThreads, payload.assistant_thread);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const channelId = payload.channel_id ?? payload.channel;
|
|
487
|
+
if (channelId) {
|
|
488
|
+
handleAgentContextChanged(this.assistantThreads, {
|
|
489
|
+
channel_id: channelId,
|
|
490
|
+
context: payload.context,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
371
494
|
async postInThread(channel, threadTs, text) {
|
|
372
495
|
return slackRetry(async () => {
|
|
373
496
|
const payload = {
|
|
@@ -885,10 +1008,27 @@ export class SlackMessagingBot {
|
|
|
885
1008
|
this.socketClient.on("unable_to_socket_mode_start", (err) => {
|
|
886
1009
|
log.logWarning("Slack socket unable_to_start", err ? String(err) : "");
|
|
887
1010
|
});
|
|
1011
|
+
// Which events Slack actually delivers is otherwise unknowable from here:
|
|
1012
|
+
// a listener that never fires looks the same whether the subscription is
|
|
1013
|
+
// missing, the app needs reinstalling, or the surface never emits it at
|
|
1014
|
+
// all. Off by default — this is one line per event on a busy workspace.
|
|
1015
|
+
if (readEnv("SLACK_TRACE_EVENTS")) {
|
|
1016
|
+
this.socketClient.on("slack_event", ({ type, body }) => {
|
|
1017
|
+
const inner = body?.event;
|
|
1018
|
+
log.logInfo(`Slack socket event: ${type ?? "?"}${inner?.type ? ` → ${inner.type}` : ""}${inner?.tab ? ` (tab: ${inner.tab})` : ""}`);
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
888
1021
|
this.socketClient.on("app_mention", (payload) => this.handleAppMention(payload));
|
|
889
1022
|
this.socketClient.on("message", (payload) => this.handleMessageEvent(payload));
|
|
890
1023
|
this.socketClient.on("slash_commands", (payload) => void this.handleSlashCommand(payload));
|
|
891
1024
|
this.socketClient.on("app_home_opened", (payload) => this.handleAppHomeOpened(payload));
|
|
1025
|
+
// The assistant surface's lifecycle. Both events were already subscribed
|
|
1026
|
+
// in the app manifest and then dropped on the floor, which is why the pane
|
|
1027
|
+
// opened empty and the sidebar filled with untitled conversations.
|
|
1028
|
+
this.socketClient.on("assistant_thread_started", (payload) => this.handleAssistantThreadStarted(payload));
|
|
1029
|
+
this.socketClient.on("assistant_thread_context_changed", (payload) => this.handleAgentContextChangedEvent(payload));
|
|
1030
|
+
// agent_view's spelling of the same signal.
|
|
1031
|
+
this.socketClient.on("app_context_changed", (payload) => this.handleAgentContextChangedEvent(payload));
|
|
892
1032
|
this.socketClient.on("block_actions", (payload) => void this.handleBlockAction(payload));
|
|
893
1033
|
this.socketClient.on("interactive", (payload) => void this.handleBlockAction(payload));
|
|
894
1034
|
}
|
|
@@ -992,6 +1132,12 @@ export class SlackMessagingBot {
|
|
|
992
1132
|
}
|
|
993
1133
|
const isThreadReply = !!e.thread_ts;
|
|
994
1134
|
const sessionKey = isDM ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
|
|
1135
|
+
// Name the conversation from the first thing the person said. Guarded on
|
|
1136
|
+
// the assistant registry, so classic DM threads are untouched, and it
|
|
1137
|
+
// claims the title once per thread.
|
|
1138
|
+
if (isDM && e.thread_ts && e.text) {
|
|
1139
|
+
void titleAssistantThread(this.assistantOps(), this.assistantThreads, e.channel, e.thread_ts, this.stripOwnMention(e.text));
|
|
1140
|
+
}
|
|
995
1141
|
const slackEvent = createConversationEvent({
|
|
996
1142
|
platform: "slack",
|
|
997
1143
|
conversationId: e.channel,
|
|
@@ -1072,6 +1218,16 @@ export class SlackMessagingBot {
|
|
|
1072
1218
|
handleAppHomeOpened({ event, ack }) {
|
|
1073
1219
|
const e = event;
|
|
1074
1220
|
ack();
|
|
1221
|
+
// Under agent_view this is how Slack says "the person opened the app's
|
|
1222
|
+
// DM" — the signal assistant_thread_started used to carry. It fires on
|
|
1223
|
+
// every open, not once per conversation, so this path refreshes the
|
|
1224
|
+
// pinned prompts and deliberately does not greet.
|
|
1225
|
+
if (e.tab === "messages") {
|
|
1226
|
+
if (e.channel) {
|
|
1227
|
+
void handleAgentDmOpened(this.assistantOps(), this.assistantThreads, e.channel, e.context);
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1075
1231
|
if (e.tab !== "home")
|
|
1076
1232
|
return;
|
|
1077
1233
|
this.webClient.views
|