@copilotkit/channels-slack 0.1.0
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/LICENSE +21 -0
- package/README.md +353 -0
- package/dist/__tests__/assistant.test.d.ts +2 -0
- package/dist/__tests__/assistant.test.d.ts.map +1 -0
- package/dist/__tests__/assistant.test.js +153 -0
- package/dist/__tests__/auto-close-streaming.test.d.ts +2 -0
- package/dist/__tests__/auto-close-streaming.test.d.ts.map +1 -0
- package/dist/__tests__/auto-close-streaming.test.js +139 -0
- package/dist/__tests__/built-ins.test.d.ts +2 -0
- package/dist/__tests__/built-ins.test.d.ts.map +1 -0
- package/dist/__tests__/built-ins.test.js +67 -0
- package/dist/__tests__/chunked-message-stream.test.d.ts +2 -0
- package/dist/__tests__/chunked-message-stream.test.d.ts.map +1 -0
- package/dist/__tests__/chunked-message-stream.test.js +226 -0
- package/dist/__tests__/conversation-store.test.d.ts +2 -0
- package/dist/__tests__/conversation-store.test.d.ts.map +1 -0
- package/dist/__tests__/conversation-store.test.js +209 -0
- package/dist/__tests__/download-files.test.d.ts +2 -0
- package/dist/__tests__/download-files.test.d.ts.map +1 -0
- package/dist/__tests__/download-files.test.js +142 -0
- package/dist/__tests__/ephemeral.test.d.ts +2 -0
- package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
- package/dist/__tests__/ephemeral.test.js +85 -0
- package/dist/__tests__/event-renderer.test.d.ts +2 -0
- package/dist/__tests__/event-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/event-renderer.test.js +521 -0
- package/dist/__tests__/markdown-to-mrkdwn.test.d.ts +2 -0
- package/dist/__tests__/markdown-to-mrkdwn.test.d.ts.map +1 -0
- package/dist/__tests__/markdown-to-mrkdwn.test.js +80 -0
- package/dist/__tests__/message-stream.test.d.ts +2 -0
- package/dist/__tests__/message-stream.test.d.ts.map +1 -0
- package/dist/__tests__/message-stream.test.js +116 -0
- package/dist/__tests__/native-renderer.test.d.ts +2 -0
- package/dist/__tests__/native-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/native-renderer.test.js +236 -0
- package/dist/__tests__/native-stream.test.d.ts +2 -0
- package/dist/__tests__/native-stream.test.d.ts.map +1 -0
- package/dist/__tests__/native-stream.test.js +235 -0
- package/dist/__tests__/reactions.test.d.ts +2 -0
- package/dist/__tests__/reactions.test.d.ts.map +1 -0
- package/dist/__tests__/reactions.test.js +193 -0
- package/dist/__tests__/slack-listener.test.d.ts +2 -0
- package/dist/__tests__/slack-listener.test.d.ts.map +1 -0
- package/dist/__tests__/slack-listener.test.js +559 -0
- package/dist/__tests__/views.test.d.ts +2 -0
- package/dist/__tests__/views.test.d.ts.map +1 -0
- package/dist/__tests__/views.test.js +143 -0
- package/dist/adapter.d.ts +188 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +859 -0
- package/dist/adapter.test.d.ts +2 -0
- package/dist/adapter.test.d.ts.map +1 -0
- package/dist/adapter.test.js +368 -0
- package/dist/assistant.d.ts +19 -0
- package/dist/assistant.d.ts.map +1 -0
- package/dist/assistant.js +124 -0
- package/dist/auto-close-streaming.d.ts +62 -0
- package/dist/auto-close-streaming.d.ts.map +1 -0
- package/dist/auto-close-streaming.js +273 -0
- package/dist/built-in-context.d.ts +22 -0
- package/dist/built-in-context.d.ts.map +1 -0
- package/dist/built-in-context.js +65 -0
- package/dist/built-in-tools.d.ts +23 -0
- package/dist/built-in-tools.d.ts.map +1 -0
- package/dist/built-in-tools.js +45 -0
- package/dist/chunked-message-stream.d.ts +67 -0
- package/dist/chunked-message-stream.d.ts.map +1 -0
- package/dist/chunked-message-stream.js +134 -0
- package/dist/codec.d.ts +18 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +18 -0
- package/dist/codec.test.d.ts +2 -0
- package/dist/codec.test.d.ts.map +1 -0
- package/dist/codec.test.js +19 -0
- package/dist/conversation-store.d.ts +92 -0
- package/dist/conversation-store.d.ts.map +1 -0
- package/dist/conversation-store.js +196 -0
- package/dist/download-files.d.ts +59 -0
- package/dist/download-files.d.ts.map +1 -0
- package/dist/download-files.js +116 -0
- package/dist/event-renderer.d.ts +98 -0
- package/dist/event-renderer.d.ts.map +1 -0
- package/dist/event-renderer.js +493 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/ingress-normalize.d.ts +75 -0
- package/dist/ingress-normalize.d.ts.map +1 -0
- package/dist/ingress-normalize.js +153 -0
- package/dist/ingress-normalize.test.d.ts +2 -0
- package/dist/ingress-normalize.test.d.ts.map +1 -0
- package/dist/ingress-normalize.test.js +187 -0
- package/dist/interaction.d.ts +30 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +219 -0
- package/dist/interaction.test.d.ts +2 -0
- package/dist/interaction.test.d.ts.map +1 -0
- package/dist/interaction.test.js +137 -0
- package/dist/markdown-to-mrkdwn.d.ts +21 -0
- package/dist/markdown-to-mrkdwn.d.ts.map +1 -0
- package/dist/markdown-to-mrkdwn.js +99 -0
- package/dist/message-stream.d.ts +44 -0
- package/dist/message-stream.d.ts.map +1 -0
- package/dist/message-stream.js +70 -0
- package/dist/native-stream.d.ts +131 -0
- package/dist/native-stream.d.ts.map +1 -0
- package/dist/native-stream.js +208 -0
- package/dist/render/block-kit.d.ts +35 -0
- package/dist/render/block-kit.d.ts.map +1 -0
- package/dist/render/block-kit.js +399 -0
- package/dist/render/block-kit.test.d.ts +2 -0
- package/dist/render/block-kit.test.d.ts.map +1 -0
- package/dist/render/block-kit.test.js +289 -0
- package/dist/render/budget.d.ts +24 -0
- package/dist/render/budget.d.ts.map +1 -0
- package/dist/render/budget.js +30 -0
- package/dist/render/budget.test.d.ts +2 -0
- package/dist/render/budget.test.d.ts.map +1 -0
- package/dist/render/budget.test.js +49 -0
- package/dist/render/modal.d.ts +13 -0
- package/dist/render/modal.d.ts.map +1 -0
- package/dist/render/modal.js +108 -0
- package/dist/render/transport.d.ts +42 -0
- package/dist/render/transport.d.ts.map +1 -0
- package/dist/render/transport.js +1 -0
- package/dist/render.d.ts +23 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +20 -0
- package/dist/sanitizing-http-agent.d.ts +28 -0
- package/dist/sanitizing-http-agent.d.ts.map +1 -0
- package/dist/sanitizing-http-agent.js +51 -0
- package/dist/slack-listener.d.ts +73 -0
- package/dist/slack-listener.d.ts.map +1 -0
- package/dist/slack-listener.js +126 -0
- package/dist/slack-listener.test.d.ts +2 -0
- package/dist/slack-listener.test.d.ts.map +1 -0
- package/dist/slack-listener.test.js +115 -0
- package/dist/types.d.ts +153 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/package.json +73 -0
package/dist/codec.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { renderSlackMessage } from "./render/block-kit.js";
|
|
2
|
+
import { normalizeSlackEvent } from "./ingress-normalize.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pure Slack codec — both directions, no Bolt and no Slack credentials. Shared
|
|
5
|
+
* by the local Slack adapter and (via the Connector Outbox / webhook ingress,
|
|
6
|
+
* OSS-362/363) the managed path, so platform semantics live in one place:
|
|
7
|
+
*
|
|
8
|
+
* - `renderEgress`: IR → Block Kit.
|
|
9
|
+
* - `normalizeIngress`: raw Slack payload (Events API envelope or slash-command
|
|
10
|
+
* body) → platform-neutral ingress event. `normalizeIngress` is a Slack
|
|
11
|
+
* extension on top of the generic egress-only {@link PlatformCodec}; the
|
|
12
|
+
* caller applies its own policy/entitlement gating and builds the route.
|
|
13
|
+
*/
|
|
14
|
+
export const slackCodec = {
|
|
15
|
+
platform: "slack",
|
|
16
|
+
renderEgress: renderSlackMessage,
|
|
17
|
+
normalizeIngress: normalizeSlackEvent,
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.test.d.ts","sourceRoot":"","sources":["../src/codec.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { slackCodec } from "./codec.js";
|
|
3
|
+
describe("slackCodec", () => {
|
|
4
|
+
it("is the slack platform codec", () => {
|
|
5
|
+
expect(slackCodec.platform).toBe("slack");
|
|
6
|
+
});
|
|
7
|
+
it("renders IR to Slack Block Kit via the shared pure renderer", () => {
|
|
8
|
+
const ir = [
|
|
9
|
+
{
|
|
10
|
+
type: "section",
|
|
11
|
+
props: { children: [{ type: "text", props: { value: "hi" } }] },
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
const out = slackCodec.renderEgress(ir);
|
|
15
|
+
expect(out.blocks).toEqual([
|
|
16
|
+
{ type: "section", text: { type: "mrkdwn", text: "hi" } },
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { HttpAgent } from "@ag-ui/client";
|
|
2
|
+
import type { WebClient } from "@slack/web-api";
|
|
3
|
+
import type { ConversationKey, ReplyTarget } from "./types.js";
|
|
4
|
+
import { type FileDeliveryConfig } from "./download-files.js";
|
|
5
|
+
/**
|
|
6
|
+
* One ongoing Slack conversation with the bot. Built fresh per turn from
|
|
7
|
+
* Slack's stored history — Slack itself is our durable storage, so the
|
|
8
|
+
* bridge keeps no separate state.
|
|
9
|
+
*/
|
|
10
|
+
export interface AgentSession {
|
|
11
|
+
threadId: string;
|
|
12
|
+
agent: HttpAgent;
|
|
13
|
+
replyTarget: ReplyTarget;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Backed entirely by Slack: every turn pulls the current thread (or DM)
|
|
17
|
+
* history via the Web API, translates it into the AG-UI message shape,
|
|
18
|
+
* and hands a fresh HttpAgent to the turn-runner. Bridge restarts are
|
|
19
|
+
* automatically robust because the bridge holds no state to lose.
|
|
20
|
+
*
|
|
21
|
+
* The only in-memory state is a participation cache — a `Set` of thread
|
|
22
|
+
* keys the bot has already replied to. It's a pure performance hint
|
|
23
|
+
* (avoids one Slack API call per thread-reply event in active
|
|
24
|
+
* conversations) and is rebuilt lazily after a restart by the same
|
|
25
|
+
* Slack lookup that already produces the answer.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SlackConversationStore {
|
|
28
|
+
private readonly client;
|
|
29
|
+
private readonly botUserId;
|
|
30
|
+
/** Bot token used to download uploaded files from their `url_private`. */
|
|
31
|
+
private readonly botToken;
|
|
32
|
+
private readonly filesConfig;
|
|
33
|
+
/** Stable threadIds → conversation keys ("channelId::scope"). */
|
|
34
|
+
private readonly participated;
|
|
35
|
+
constructor(args: {
|
|
36
|
+
client: WebClient;
|
|
37
|
+
botUserId: string;
|
|
38
|
+
botToken: string;
|
|
39
|
+
files?: FileDeliveryConfig;
|
|
40
|
+
});
|
|
41
|
+
private keyOf;
|
|
42
|
+
/**
|
|
43
|
+
* A *fresh* AG-UI threadId per turn.
|
|
44
|
+
*
|
|
45
|
+
* We deliberately do NOT reuse a stable per-conversation threadId.
|
|
46
|
+
* Slack is our durable history (every turn is rebuilt from it via
|
|
47
|
+
* {@link fetchHistory}), so the LangGraph thread only needs to live for
|
|
48
|
+
* the duration of one turn. Reusing a stable threadId across turns lets
|
|
49
|
+
* the server-side thread accumulate the agent's *internal* messages
|
|
50
|
+
* (tool calls/results that never round-trip through Slack); on the next
|
|
51
|
+
* turn `@ag-ui/langgraph` regenerates state and the now-larger existing
|
|
52
|
+
* thread no longer matches the incoming history, surfacing as a
|
|
53
|
+
* "Message not found" failure. A unique thread per turn sidesteps that
|
|
54
|
+
* entirely. Restart-recovery for interrupts still works because the
|
|
55
|
+
* picker carries its turn's threadId in Slack message metadata (see
|
|
56
|
+
* `recoverFromStaleClick`).
|
|
57
|
+
*/
|
|
58
|
+
private newThreadId;
|
|
59
|
+
/**
|
|
60
|
+
* Does the bot own this thread? "Ownership" means the bot has at least
|
|
61
|
+
* one prior reply in it (which is the natural definition since the bot
|
|
62
|
+
* only ever replies when @-mentioned or in a thread it already owns).
|
|
63
|
+
*
|
|
64
|
+
* Cached in-process; the first call after a restart is one Slack API
|
|
65
|
+
* round-trip, subsequent calls are O(1).
|
|
66
|
+
*/
|
|
67
|
+
has(key: ConversationKey): Promise<boolean>;
|
|
68
|
+
/**
|
|
69
|
+
* Build a fresh AgentSession for this conversation by fetching its
|
|
70
|
+
* Slack history and translating it into the AG-UI message shape.
|
|
71
|
+
* `makeAgent` produces the HttpAgent; we set its `messages` to the
|
|
72
|
+
* translated history.
|
|
73
|
+
*/
|
|
74
|
+
getOrCreate(key: ConversationKey, replyTarget: ReplyTarget, makeAgent: (threadId: string) => HttpAgent): Promise<AgentSession>;
|
|
75
|
+
/**
|
|
76
|
+
* No-op: the next call rebuilds from Slack, so there's nothing to
|
|
77
|
+
* persist. Kept on the API for symmetry with the previous interface
|
|
78
|
+
* (the turn-runner still calls it; we just do nothing).
|
|
79
|
+
*/
|
|
80
|
+
save(_key: ConversationKey, _session: AgentSession): void;
|
|
81
|
+
/** Fetch Slack history for either a thread or a DM and translate. */
|
|
82
|
+
private fetchHistory;
|
|
83
|
+
/**
|
|
84
|
+
* Translate a chronological run of Slack messages to AG-UI messages,
|
|
85
|
+
* folding (a) our bot's chunked replies into a single assistant turn,
|
|
86
|
+
* (b) skipping our `:wrench:` / `:white_check_mark:` status messages
|
|
87
|
+
* and the `_thinking…_` placeholder, and (c) stripping `<@bot>`
|
|
88
|
+
* mention tokens from user text.
|
|
89
|
+
*/
|
|
90
|
+
private translate;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=conversation-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-store.d.ts","sourceRoot":"","sources":["../src/conversation-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE/D,OAAO,EAGL,KAAK,kBAAkB,EAExB,MAAM,qBAAqB,CAAC;AAE7B;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;gBAEtC,IAAI,EAAE;QAChB,MAAM,EAAE,SAAS,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,kBAAkB,CAAC;KAC5B;IAOD,OAAO,CAAC,KAAK;IAIb;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,WAAW;IAInB;;;;;;;OAOG;IACG,GAAG,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IAqBjD;;;;;OAKG;IACG,WAAW,CACf,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,SAAS,GACzC,OAAO,CAAC,YAAY,CAAC;IAWxB;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,GAAG,IAAI;IAIzD,qEAAqE;YACvD,YAAY;IAsB1B;;;;;;OAMG;YACW,SAAS;CA4DxB"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { DM_SCOPE } from "./types.js";
|
|
3
|
+
import { buildFileContentParts, } from "./download-files.js";
|
|
4
|
+
/**
|
|
5
|
+
* Backed entirely by Slack: every turn pulls the current thread (or DM)
|
|
6
|
+
* history via the Web API, translates it into the AG-UI message shape,
|
|
7
|
+
* and hands a fresh HttpAgent to the turn-runner. Bridge restarts are
|
|
8
|
+
* automatically robust because the bridge holds no state to lose.
|
|
9
|
+
*
|
|
10
|
+
* The only in-memory state is a participation cache — a `Set` of thread
|
|
11
|
+
* keys the bot has already replied to. It's a pure performance hint
|
|
12
|
+
* (avoids one Slack API call per thread-reply event in active
|
|
13
|
+
* conversations) and is rebuilt lazily after a restart by the same
|
|
14
|
+
* Slack lookup that already produces the answer.
|
|
15
|
+
*/
|
|
16
|
+
export class SlackConversationStore {
|
|
17
|
+
client;
|
|
18
|
+
botUserId;
|
|
19
|
+
/** Bot token used to download uploaded files from their `url_private`. */
|
|
20
|
+
botToken;
|
|
21
|
+
filesConfig;
|
|
22
|
+
/** Stable threadIds → conversation keys ("channelId::scope"). */
|
|
23
|
+
participated = new Set();
|
|
24
|
+
constructor(args) {
|
|
25
|
+
this.client = args.client;
|
|
26
|
+
this.botUserId = args.botUserId;
|
|
27
|
+
this.botToken = args.botToken;
|
|
28
|
+
this.filesConfig = args.files ?? {};
|
|
29
|
+
}
|
|
30
|
+
keyOf(k) {
|
|
31
|
+
return `${k.channelId}::${k.scope}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A *fresh* AG-UI threadId per turn.
|
|
35
|
+
*
|
|
36
|
+
* We deliberately do NOT reuse a stable per-conversation threadId.
|
|
37
|
+
* Slack is our durable history (every turn is rebuilt from it via
|
|
38
|
+
* {@link fetchHistory}), so the LangGraph thread only needs to live for
|
|
39
|
+
* the duration of one turn. Reusing a stable threadId across turns lets
|
|
40
|
+
* the server-side thread accumulate the agent's *internal* messages
|
|
41
|
+
* (tool calls/results that never round-trip through Slack); on the next
|
|
42
|
+
* turn `@ag-ui/langgraph` regenerates state and the now-larger existing
|
|
43
|
+
* thread no longer matches the incoming history, surfacing as a
|
|
44
|
+
* "Message not found" failure. A unique thread per turn sidesteps that
|
|
45
|
+
* entirely. Restart-recovery for interrupts still works because the
|
|
46
|
+
* picker carries its turn's threadId in Slack message metadata (see
|
|
47
|
+
* `recoverFromStaleClick`).
|
|
48
|
+
*/
|
|
49
|
+
newThreadId(k) {
|
|
50
|
+
return `slack-${k.channelId}-${k.scope}-${randomUUID()}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Does the bot own this thread? "Ownership" means the bot has at least
|
|
54
|
+
* one prior reply in it (which is the natural definition since the bot
|
|
55
|
+
* only ever replies when @-mentioned or in a thread it already owns).
|
|
56
|
+
*
|
|
57
|
+
* Cached in-process; the first call after a restart is one Slack API
|
|
58
|
+
* round-trip, subsequent calls are O(1).
|
|
59
|
+
*/
|
|
60
|
+
async has(key) {
|
|
61
|
+
if (this.participated.has(this.keyOf(key)))
|
|
62
|
+
return true;
|
|
63
|
+
// DM "scope" is a sentinel — no Slack thread to query. We treat the
|
|
64
|
+
// listener-level DM gate as authoritative; DMs always go through.
|
|
65
|
+
if (key.scope === DM_SCOPE)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
const r = await this.client.conversations.replies({
|
|
69
|
+
channel: key.channelId,
|
|
70
|
+
ts: key.scope,
|
|
71
|
+
limit: 200,
|
|
72
|
+
});
|
|
73
|
+
const messages = (r.messages ?? []);
|
|
74
|
+
const owned = messages.some((m) => m.user === this.botUserId);
|
|
75
|
+
if (owned)
|
|
76
|
+
this.participated.add(this.keyOf(key));
|
|
77
|
+
return owned;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.warn("[store] has() lookup failed:", err);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a fresh AgentSession for this conversation by fetching its
|
|
86
|
+
* Slack history and translating it into the AG-UI message shape.
|
|
87
|
+
* `makeAgent` produces the HttpAgent; we set its `messages` to the
|
|
88
|
+
* translated history.
|
|
89
|
+
*/
|
|
90
|
+
async getOrCreate(key, replyTarget, makeAgent) {
|
|
91
|
+
const threadId = this.newThreadId(key);
|
|
92
|
+
const agent = makeAgent(threadId);
|
|
93
|
+
const history = await this.fetchHistory(key);
|
|
94
|
+
agent.messages = history;
|
|
95
|
+
// Eagerly mark as ours so a follow-up arriving before the bot's first
|
|
96
|
+
// reply has settled into Slack still gets matched as "owned".
|
|
97
|
+
this.participated.add(this.keyOf(key));
|
|
98
|
+
return { threadId, agent, replyTarget };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* No-op: the next call rebuilds from Slack, so there's nothing to
|
|
102
|
+
* persist. Kept on the API for symmetry with the previous interface
|
|
103
|
+
* (the turn-runner still calls it; we just do nothing).
|
|
104
|
+
*/
|
|
105
|
+
save(_key, _session) {
|
|
106
|
+
/* intentionally empty */
|
|
107
|
+
}
|
|
108
|
+
/** Fetch Slack history for either a thread or a DM and translate. */
|
|
109
|
+
async fetchHistory(key) {
|
|
110
|
+
try {
|
|
111
|
+
if (key.scope === DM_SCOPE) {
|
|
112
|
+
const r = await this.client.conversations.history({
|
|
113
|
+
channel: key.channelId,
|
|
114
|
+
limit: 100,
|
|
115
|
+
});
|
|
116
|
+
const slackMsgs = (r.messages ?? []).reverse(); // oldest first
|
|
117
|
+
return this.translate(slackMsgs);
|
|
118
|
+
}
|
|
119
|
+
const r = await this.client.conversations.replies({
|
|
120
|
+
channel: key.channelId,
|
|
121
|
+
ts: key.scope,
|
|
122
|
+
limit: 200,
|
|
123
|
+
});
|
|
124
|
+
return this.translate((r.messages ?? []));
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.warn("[store] fetchHistory failed:", err);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Translate a chronological run of Slack messages to AG-UI messages,
|
|
133
|
+
* folding (a) our bot's chunked replies into a single assistant turn,
|
|
134
|
+
* (b) skipping our `:wrench:` / `:white_check_mark:` status messages
|
|
135
|
+
* and the `_thinking…_` placeholder, and (c) stripping `<@bot>`
|
|
136
|
+
* mention tokens from user text.
|
|
137
|
+
*/
|
|
138
|
+
async translate(messages) {
|
|
139
|
+
const MENTION_RE = /<@[UW][A-Z0-9]+>/g;
|
|
140
|
+
const isStatusOrPlaceholder = (text) => text.startsWith(":wrench:") ||
|
|
141
|
+
text.startsWith(":white_check_mark:") ||
|
|
142
|
+
text === "_thinking…_" ||
|
|
143
|
+
text === "_…(continued)_";
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const m of messages) {
|
|
146
|
+
if (m.subtype && m.subtype !== "file_share")
|
|
147
|
+
continue;
|
|
148
|
+
// Bot's own messages: matched by user id, or by bot_id when the API
|
|
149
|
+
// omits `user` (legacy bot integrations). A user-token app message has
|
|
150
|
+
// BOTH user and bot_id — `!m.user` keeps those classified as user.
|
|
151
|
+
const isBot = m.user === this.botUserId || (!!m.bot_id && !m.user);
|
|
152
|
+
const hasFiles = !isBot && Array.isArray(m.files) && m.files.length > 0;
|
|
153
|
+
if (!m.text && !hasFiles)
|
|
154
|
+
continue;
|
|
155
|
+
if (isBot && isStatusOrPlaceholder(m.text ?? ""))
|
|
156
|
+
continue;
|
|
157
|
+
let text = m.text ?? "";
|
|
158
|
+
if (!isBot) {
|
|
159
|
+
text = text.replace(MENTION_RE, "").replace(/\s+/g, " ").trim();
|
|
160
|
+
}
|
|
161
|
+
const role = isBot ? "assistant" : "user";
|
|
162
|
+
// User message with uploaded files → multimodal content (the agent's
|
|
163
|
+
// model reads the images / decoded text). The bridge only delivers;
|
|
164
|
+
// the app decides what to do with the bytes.
|
|
165
|
+
if (hasFiles) {
|
|
166
|
+
const { parts, notes } = await buildFileContentParts(m.files, this.botToken, this.filesConfig);
|
|
167
|
+
const content = [];
|
|
168
|
+
if (text)
|
|
169
|
+
content.push({ type: "text", text });
|
|
170
|
+
content.push(...parts);
|
|
171
|
+
if (notes.length > 0) {
|
|
172
|
+
content.push({
|
|
173
|
+
type: "text",
|
|
174
|
+
text: `[attachment notes: ${notes.join("; ")}]`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (content.length === 0)
|
|
178
|
+
continue; // nothing usable
|
|
179
|
+
out.push({ id: m.ts ?? "", role, content });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!text)
|
|
183
|
+
continue;
|
|
184
|
+
// Fold consecutive same-role *string* messages — our chunked bot
|
|
185
|
+
// replies are one assistant turn, just rendered as N Slack messages.
|
|
186
|
+
const tail = out[out.length - 1];
|
|
187
|
+
if (tail && tail.role === role && typeof tail.content === "string") {
|
|
188
|
+
tail.content = `${tail.content}\n${text}`;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
out.push({ id: m.ts ?? "", role, content: text });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** The subset of a Slack file object we use. */
|
|
2
|
+
export interface SlackFileRef {
|
|
3
|
+
id?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
mimetype?: string;
|
|
6
|
+
filetype?: string;
|
|
7
|
+
url_private?: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
}
|
|
10
|
+
/** A base64 data source, shared by every binary media part. */
|
|
11
|
+
type MediaDataSource = {
|
|
12
|
+
type: "data";
|
|
13
|
+
value: string;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* AG-UI multimodal content parts (shape the runtime's converter expects).
|
|
18
|
+
*
|
|
19
|
+
* Binary media (image/audio/video/document) is passed straight through as a
|
|
20
|
+
* data part; the agent's model decides what it can actually consume. Most
|
|
21
|
+
* models read images and PDFs; far fewer accept audio or video. The bridge
|
|
22
|
+
* stays transport-only and does not gate on model capability.
|
|
23
|
+
*/
|
|
24
|
+
export type AgentContentPart = {
|
|
25
|
+
type: "text";
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: "image";
|
|
29
|
+
source: MediaDataSource;
|
|
30
|
+
} | {
|
|
31
|
+
type: "audio";
|
|
32
|
+
source: MediaDataSource;
|
|
33
|
+
} | {
|
|
34
|
+
type: "video";
|
|
35
|
+
source: MediaDataSource;
|
|
36
|
+
} | {
|
|
37
|
+
type: "document";
|
|
38
|
+
source: MediaDataSource;
|
|
39
|
+
};
|
|
40
|
+
/** Tunables for inbound file handling (all optional; sane defaults). */
|
|
41
|
+
export interface FileDeliveryConfig {
|
|
42
|
+
/** Skip a single file larger than this many bytes. Default 8 MiB. */
|
|
43
|
+
maxBytesPerFile?: number;
|
|
44
|
+
/** Process at most this many files per message. Default 5. */
|
|
45
|
+
maxFiles?: number;
|
|
46
|
+
/** Truncate decoded text files to this many bytes. Default 200 KiB. */
|
|
47
|
+
maxTextBytes?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Download a message's files and turn them into AG-UI content parts. Returns
|
|
51
|
+
* the parts plus human-readable `notes` for anything skipped (appended to the
|
|
52
|
+
* message as a text part by the caller).
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildFileContentParts(files: SlackFileRef[], botToken: string, config?: FileDeliveryConfig): Promise<{
|
|
55
|
+
parts: AgentContentPart[];
|
|
56
|
+
notes: string[];
|
|
57
|
+
}>;
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=download-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-files.d.ts","sourceRoot":"","sources":["../src/download-files.ts"],"names":[],"mappings":"AAaA,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,+DAA+D;AAC/D,KAAK,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC;AAElD,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAwCD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,YAAY,EAAE,EACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,kBAAuB,GAC9B,OAAO,CAAC;IAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAwFzD"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
maxBytesPerFile: 8 * 1024 * 1024,
|
|
3
|
+
maxFiles: 5,
|
|
4
|
+
maxTextBytes: 200 * 1024,
|
|
5
|
+
};
|
|
6
|
+
const TEXT_MIME_PREFIXES = ["text/"];
|
|
7
|
+
const TEXT_MIME_EXACT = new Set([
|
|
8
|
+
"application/json",
|
|
9
|
+
"application/csv",
|
|
10
|
+
"application/xml",
|
|
11
|
+
"application/x-ndjson",
|
|
12
|
+
"application/yaml",
|
|
13
|
+
]);
|
|
14
|
+
function isText(mime) {
|
|
15
|
+
return (TEXT_MIME_PREFIXES.some((p) => mime.startsWith(p)) ||
|
|
16
|
+
TEXT_MIME_EXACT.has(mime));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The AG-UI media part type that carries this MIME, or null if it isn't a
|
|
20
|
+
* binary medium we pass through. Images/audio/video go by their top-level
|
|
21
|
+
* type; PDFs map to `document`. Everything else is left to the text path or
|
|
22
|
+
* skipped.
|
|
23
|
+
*/
|
|
24
|
+
function mediaPartType(mime) {
|
|
25
|
+
if (mime.startsWith("image/"))
|
|
26
|
+
return "image";
|
|
27
|
+
if (mime.startsWith("audio/"))
|
|
28
|
+
return "audio";
|
|
29
|
+
if (mime.startsWith("video/"))
|
|
30
|
+
return "video";
|
|
31
|
+
if (mime === "application/pdf")
|
|
32
|
+
return "document";
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Download a message's files and turn them into AG-UI content parts. Returns
|
|
37
|
+
* the parts plus human-readable `notes` for anything skipped (appended to the
|
|
38
|
+
* message as a text part by the caller).
|
|
39
|
+
*/
|
|
40
|
+
export async function buildFileContentParts(files, botToken, config = {}) {
|
|
41
|
+
const maxBytes = config.maxBytesPerFile ?? DEFAULTS.maxBytesPerFile;
|
|
42
|
+
const maxFiles = config.maxFiles ?? DEFAULTS.maxFiles;
|
|
43
|
+
const maxText = config.maxTextBytes ?? DEFAULTS.maxTextBytes;
|
|
44
|
+
const parts = [];
|
|
45
|
+
const notes = [];
|
|
46
|
+
const considered = files.slice(0, maxFiles);
|
|
47
|
+
if (files.length > maxFiles) {
|
|
48
|
+
notes.push(`(only the first ${maxFiles} of ${files.length} files processed)`);
|
|
49
|
+
}
|
|
50
|
+
for (const f of considered) {
|
|
51
|
+
const label = f.name ?? f.id ?? "file";
|
|
52
|
+
const mime = (f.mimetype ?? "").toLowerCase();
|
|
53
|
+
const media = mediaPartType(mime);
|
|
54
|
+
if (!f.url_private) {
|
|
55
|
+
notes.push(`skipped "${label}": no download URL`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!media && !isText(mime)) {
|
|
59
|
+
notes.push(`skipped "${label}" (${mime || f.filetype || "unknown"}): unsupported type`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (typeof f.size === "number" && f.size > maxBytes) {
|
|
63
|
+
notes.push(`skipped "${label}": ${f.size} bytes exceeds the ${maxBytes}-byte cap`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
let bytes;
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(f.url_private, {
|
|
69
|
+
headers: { Authorization: `Bearer ${botToken}` },
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
notes.push(`skipped "${label}": download failed (HTTP ${res.status})`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
bytes = Buffer.from(await res.arrayBuffer());
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
notes.push(`skipped "${label}": ${err.message}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (bytes.byteLength > maxBytes) {
|
|
82
|
+
notes.push(`skipped "${label}": ${bytes.byteLength} bytes exceeds the ${maxBytes}-byte cap`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (media) {
|
|
86
|
+
// Image/audio/video/PDF → a binary data part the model reads natively
|
|
87
|
+
// (subject to its modality support). The bridge just delivers it.
|
|
88
|
+
parts.push({
|
|
89
|
+
type: media,
|
|
90
|
+
source: {
|
|
91
|
+
type: "data",
|
|
92
|
+
value: bytes.toString("base64"),
|
|
93
|
+
mimeType: mime,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Truncate the BYTES then decode — slicing the decoded string by
|
|
99
|
+
// character index would corrupt multi-byte UTF-8 (and not actually
|
|
100
|
+
// bound the byte length). toString drops any malformed trailing bytes.
|
|
101
|
+
let buf = bytes;
|
|
102
|
+
let truncated = false;
|
|
103
|
+
if (buf.byteLength > maxText) {
|
|
104
|
+
buf = buf.subarray(0, maxText);
|
|
105
|
+
truncated = true;
|
|
106
|
+
}
|
|
107
|
+
const text = buf.toString("utf8");
|
|
108
|
+
parts.push({
|
|
109
|
+
type: "text",
|
|
110
|
+
text: `Attached file "${label}" (${mime}${truncated ? ", truncated" : ""}):\n` +
|
|
111
|
+
text,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { parts, notes };
|
|
116
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { KnownBlock } from "@slack/types";
|
|
2
|
+
import type { RunRenderer } from "@copilotkit/channels";
|
|
3
|
+
import type { NativeStreamTransport } from "./native-stream.js";
|
|
4
|
+
import type { SlackRenderTransport } from "./render/transport.js";
|
|
5
|
+
import type { SlackAssistantOptions } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Construct a {@link RunRenderer} for a single agent run in Slack.
|
|
8
|
+
*
|
|
9
|
+
* The `subscriber` is passed to `runAgent`. After the run resolves, the
|
|
10
|
+
* run-loop reads `getCapturedToolCalls()` (filtering by which tools are
|
|
11
|
+
* registered) and `getPendingInterrupt()`, then calls `finish()` once at
|
|
12
|
+
* turn end. `markInterrupted()` is called when a new turn arrives for the
|
|
13
|
+
* same conversation while this run is still streaming — it appends a
|
|
14
|
+
* `_(interrupted)_` marker to any partial reply so the user sees a clear
|
|
15
|
+
* visual cue that the bot stopped.
|
|
16
|
+
*
|
|
17
|
+
* ## Native vs legacy text streaming
|
|
18
|
+
*
|
|
19
|
+
* When `nativeStreaming` is set, the run uses a SINGLE turn-scoped
|
|
20
|
+
* `chat.startStream` message for the whole turn: text from every AG-UI
|
|
21
|
+
* message accumulates into it (separated by blank lines), and tool calls
|
|
22
|
+
* surface as native `task_update` chunks INSIDE that message. The message is
|
|
23
|
+
* finalized once, at `finish()`, optionally carrying a feedback row. The
|
|
24
|
+
* legacy path keeps the prior behavior — one `chat.update` message per AG-UI
|
|
25
|
+
* text message, plus separate `:wrench:` tool-status rows.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createRunRenderer(args: {
|
|
28
|
+
/**
|
|
29
|
+
* The credentialed Slack side-effects (setStatus / postMessage / update),
|
|
30
|
+
* injected so this renderer never imports `@slack/web-api`. The native
|
|
31
|
+
* adapter wraps a `WebClient`; the managed Connector Outbox wraps its own
|
|
32
|
+
* sender.
|
|
33
|
+
*/
|
|
34
|
+
transport: SlackRenderTransport;
|
|
35
|
+
target: {
|
|
36
|
+
channel: string;
|
|
37
|
+
threadTs?: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Custom-event names that should be treated as interrupts — captured
|
|
41
|
+
* for later dispatch to an `InterruptHandler`. Defaults to just
|
|
42
|
+
* `on_interrupt` (the name LangGraph's AG-UI adapter emits).
|
|
43
|
+
*/
|
|
44
|
+
interruptEventNames?: ReadonlySet<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Master toggle for surfacing tool-call progress in the UI. Defaults to
|
|
47
|
+
* `true`. When `false`, NO tool progress is shown on any surface — native
|
|
48
|
+
* in-message `task_update` chunks, legacy `:wrench:` rows, and the pane's
|
|
49
|
+
* "is using `tool`…" composer status are all suppressed (tools still run;
|
|
50
|
+
* only the display is hidden). When `true`, the surface is chosen by target:
|
|
51
|
+
* native streaming → `task_update` chunks; legacy / native-degraded →
|
|
52
|
+
* `:wrench:`/`:white_check_mark:` rows; pane → composer status (further
|
|
53
|
+
* gated by the pane's own `toolStatus`).
|
|
54
|
+
*
|
|
55
|
+
* Status rows dedup by `toolCallId` so a tool that fires
|
|
56
|
+
* `TOOL_CALL_START` twice (e.g. on graph resume after an interrupt)
|
|
57
|
+
* can't post two rows for the same logical call.
|
|
58
|
+
*/
|
|
59
|
+
showToolStatus?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Present whenever the reply is anchored to a thread we can set native status
|
|
62
|
+
* on (pane, channel @-mention, channel thread, or a flat DM via its carried
|
|
63
|
+
* inbound ts). Drives Slack's native `assistant.threads.setStatus`
|
|
64
|
+
* ("is thinking…") instead of a placeholder message. `threadTs` is the thread
|
|
65
|
+
* the status attaches to; `isPane` selects how *tool* progress is surfaced
|
|
66
|
+
* (pane → composer "is using `tool`…"; non-pane → `task_update`/`:wrench:`).
|
|
67
|
+
*/
|
|
68
|
+
status?: {
|
|
69
|
+
threadTs: string;
|
|
70
|
+
isPane: boolean;
|
|
71
|
+
config?: SlackAssistantOptions["status"];
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* When set, agent text replies stream via `chat.startStream` (native) using
|
|
75
|
+
* this transport instead of `chat.update`. Falls back to the legacy
|
|
76
|
+
* transport automatically if `startStream` fails. `onChunkFailure` flips the
|
|
77
|
+
* renderer to `:wrench:` tool-status rows if structured chunks are
|
|
78
|
+
* unsupported.
|
|
79
|
+
*/
|
|
80
|
+
nativeStreaming?: {
|
|
81
|
+
transport: NativeStreamTransport;
|
|
82
|
+
onStartFailure?: (err: unknown) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Whether structured `task_update` chunks are known to work on this
|
|
85
|
+
* workspace (adapter-persisted across turns). Defaults to `true`.
|
|
86
|
+
*/
|
|
87
|
+
taskChunks?: boolean;
|
|
88
|
+
/** Called the first time a chunk append fails, so the adapter can persist the degradation. */
|
|
89
|
+
onChunkFailure?: () => void;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Native AI-feedback row (built by the adapter when `feedback` is
|
|
93
|
+
* configured) attached to the finalized streamed reply via `stopStream`.
|
|
94
|
+
* Native path only; omitted when absent or on legacy fallback.
|
|
95
|
+
*/
|
|
96
|
+
feedbackBlocks?: KnownBlock[];
|
|
97
|
+
}): RunRenderer;
|
|
98
|
+
//# sourceMappingURL=event-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-renderer.d.ts","sourceRoot":"","sources":["../src/event-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EACV,WAAW,EAGZ,MAAM,sBAAsB,CAAC;AAK9B,OAAO,KAAK,EAAc,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAwBxD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC;;;;;OAKG;IACH,SAAS,EAAE,oBAAoB,CAAC;IAChC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE;QACP,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC;KAC1C,CAAC;IACF;;;;;;OAMG;IACH,eAAe,CAAC,EAAE;QAChB,SAAS,EAAE,qBAAqB,CAAC;QACjC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;QACxC;;;WAGG;QACH,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,8FAA8F;QAC9F,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;KAC7B,CAAC;IACF;;;;OAIG;IACH,cAAc,CAAC,EAAE,UAAU,EAAE,CAAC;CAC/B,GAAG,WAAW,CAwcd"}
|