@jobmatchme/bee-slack 0.1.12 → 0.1.14
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/slack-sink.d.ts +7 -0
- package/dist/slack-sink.d.ts.map +1 -1
- package/dist/slack-sink.js +56 -7
- package/dist/slack-sink.js.map +1 -1
- package/package.json +1 -1
package/dist/slack-sink.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ActionUpdate, ArtifactRef, BlobStore, TransportOutputTarget, TransportSink, TransportStreamResult, TransportStreamStart } from "@jobmatchme/bee-gate";
|
|
2
2
|
import type { WebClient } from "@slack/web-api";
|
|
3
3
|
export declare const SLACK_STREAM_HEADER = "Ich bearbeite jetzt deine Anfrage...";
|
|
4
|
+
export declare const SLACK_PLAN_ACTIVE_TITLE = "Biene Willy arbeitet \uD83D\uDC1D";
|
|
5
|
+
export declare const SLACK_PLAN_COMPLETE_TITLE = "Biene Willy ist fertig \uD83C\uDF6F";
|
|
6
|
+
export declare const SLACK_PLAN_ERROR_TITLE = "Biene Willy braucht Hilfe \uD83D\uDC1D";
|
|
7
|
+
export declare const SLACK_RUN_TASK_ID = "bee-run";
|
|
8
|
+
export declare const SLACK_RUN_TASK_TITLE = "Anfrage bearbeiten";
|
|
4
9
|
export declare const SLACK_TASK_TEXT_LIMIT = 256;
|
|
5
10
|
export declare const SLACK_MARKDOWN_LIMIT = 12000;
|
|
6
11
|
export declare const SLACK_MARKDOWN_TRUNCATION_SUFFIX = "\n\n_[Antwort auf 12.000 Zeichen gek\u00FCrzt]_";
|
|
@@ -15,6 +20,8 @@ export declare class SlackSink implements TransportSink<string> {
|
|
|
15
20
|
private blobStore;
|
|
16
21
|
private observer?;
|
|
17
22
|
constructor(webClient: WebClient, blobStore: BlobStore, observer?: SlackSinkObserver | undefined);
|
|
23
|
+
private readonly taskDetailsSent;
|
|
24
|
+
private readonly planStreams;
|
|
18
25
|
postMessage(target: TransportOutputTarget, text: string): Promise<string>;
|
|
19
26
|
updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void>;
|
|
20
27
|
prepareStreamText(text: string): string;
|
package/dist/slack-sink.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-sink.d.ts","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,YAAY,EACZ,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,aAAa,EACb,qBAAqB,EACrB,oBAAoB,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,eAAO,MAAM,mBAAmB,yCAAyC,CAAC;AAC1E,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,eAAO,MAAM,gCAAgC,oDAA+C,CAAC;AAS7F,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,MAAM,WAAW,iBAAiB;IACjC,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACvE,OAAO,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACxE;AAED,qBAAa,SAAU,YAAW,aAAa,CAAC,MAAM,CAAC;IAErD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ,CAAC;IAHlB,YACS,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,QAAQ,CAAC,+BAAmB,EACjC;
|
|
1
|
+
{"version":3,"file":"slack-sink.d.ts","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,YAAY,EACZ,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,aAAa,EACb,qBAAqB,EACrB,oBAAoB,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,eAAO,MAAM,mBAAmB,yCAAyC,CAAC;AAC1E,eAAO,MAAM,uBAAuB,sCAA2B,CAAC;AAChE,eAAO,MAAM,yBAAyB,wCAA6B,CAAC;AACpE,eAAO,MAAM,sBAAsB,2CAAgC,CAAC;AACpE,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAC3C,eAAO,MAAM,oBAAoB,uBAAuB,CAAC;AACzD,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,eAAO,MAAM,gCAAgC,oDAA+C,CAAC;AAS7F,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,MAAM,WAAW,iBAAiB;IACjC,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACvE,OAAO,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACxE;AAED,qBAAa,SAAU,YAAW,aAAa,CAAC,MAAM,CAAC;IAErD,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ,CAAC;IAHlB,YACS,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,QAAQ,CAAC,+BAAmB,EACjC;IACJ,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkC;IAClE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IAE3C,WAAW,CAAC,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;IAEK,aAAa,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3F;IAED,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtC;IAEK,WAAW,CAAC,MAAM,EAAE,qBAAqB,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqC7F;IAEK,YAAY,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlG;IAEK,UAAU,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCzG;IAEK,eAAe,CAAC,MAAM,EAAE,qBAAqB,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzF;CACD","sourcesContent":["import type {\n\tActionUpdate,\n\tArtifactRef,\n\tBlobStore,\n\tTransportOutputTarget,\n\tTransportSink,\n\tTransportStreamResult,\n\tTransportStreamStart,\n} from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport const SLACK_STREAM_HEADER = \"Ich bearbeite jetzt deine Anfrage...\";\nexport const SLACK_PLAN_ACTIVE_TITLE = \"Biene Willy arbeitet 🐝\";\nexport const SLACK_PLAN_COMPLETE_TITLE = \"Biene Willy ist fertig 🍯\";\nexport const SLACK_PLAN_ERROR_TITLE = \"Biene Willy braucht Hilfe 🐝\";\nexport const SLACK_RUN_TASK_ID = \"bee-run\";\nexport const SLACK_RUN_TASK_TITLE = \"Anfrage bearbeiten\";\nexport const SLACK_TASK_TEXT_LIMIT = 256;\nexport const SLACK_MARKDOWN_LIMIT = 12_000;\nexport const SLACK_MARKDOWN_TRUNCATION_SUFFIX = \"\\n\\n_[Antwort auf 12.000 Zeichen gekürzt]_\";\n\nfunction truncateCodePoints(text: string, limit: number, suffix: string): string {\n\tconst codePoints = Array.from(text);\n\tif (codePoints.length <= limit) return text;\n\tconst suffixCodePoints = Array.from(suffix);\n\treturn `${codePoints.slice(0, Math.max(0, limit - suffixCodePoints.length)).join(\"\")}${suffix}`;\n}\n\nexport function truncateSlackTaskText(text: string): string {\n\treturn truncateCodePoints(text, SLACK_TASK_TEXT_LIMIT, \"…\");\n}\n\nexport function truncateSlackMarkdown(text: string): string {\n\treturn truncateCodePoints(text, SLACK_MARKDOWN_LIMIT, SLACK_MARKDOWN_TRUNCATION_SUFFIX);\n}\n\nfunction requiredContextString(start: TransportStreamStart, key: string): string {\n\tconst value = start.context?.[key];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Missing Slack stream context ${key}`);\n\t}\n\treturn value;\n}\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\tprivate readonly taskDetailsSent = new Map<string, Set<string>>();\n\tprivate readonly planStreams = new Set<string>();\n\n\tasync postMessage(target: TransportOutputTarget, text: string): Promise<string> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\tchannel: target.channelId,\n\t\t\ttext,\n\t\t\tthread_ts: target.threadId,\n\t\t});\n\t\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\n\t}\n\n\tasync updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tawait this.webClient.chat.update({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\ttext,\n\t\t});\n\t\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tprepareStreamText(text: string): string {\n\t\treturn truncateSlackMarkdown(text);\n\t}\n\n\tasync startStream(target: TransportOutputTarget, start: TransportStreamStart): Promise<string> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tif (!target.threadId) throw new Error(\"Missing Slack stream thread timestamp\");\n\n\t\tconst headerChunk = {\n\t\t\ttype: \"blocks\",\n\t\t\tblocks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"header\",\n\t\t\t\t\ttext: { type: \"plain_text\", text: SLACK_STREAM_HEADER },\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst planChunks =\n\t\t\tstart.presentation === \"plan\"\n\t\t\t\t? [\n\t\t\t\t\t\t{ type: \"plan_update\", title: SLACK_PLAN_ACTIVE_TITLE },\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\t\t\tid: SLACK_RUN_TASK_ID,\n\t\t\t\t\t\t\ttitle: SLACK_RUN_TASK_TITLE,\n\t\t\t\t\t\t\tstatus: \"in_progress\",\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [];\n\t\tconst result = await this.webClient.chat.startStream({\n\t\t\tchannel: target.channelId,\n\t\t\tthread_ts: target.threadId,\n\t\t\trecipient_user_id: requiredContextString(start, \"recipientUserId\"),\n\t\t\trecipient_team_id: requiredContextString(start, \"recipientTeamId\"),\n\t\t\ttask_display_mode: start.presentation || \"timeline\",\n\t\t\t// Slack supports block chunks although the current SDK's chunk union omits them.\n\t\t\tchunks: [headerChunk, ...planChunks] as never,\n\t\t});\n\t\tif (!result.ts) throw new Error(\"Slack chat.startStream did not return a message timestamp\");\n\t\tif (start.presentation === \"plan\") this.planStreams.add(result.ts);\n\t\treturn result.ts;\n\t}\n\n\tasync updateStream(target: TransportOutputTarget, ref: string, action: ActionUpdate): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\n\t\tconst sentTaskIds = this.taskDetailsSent.get(ref) ?? new Set<string>();\n\t\tconst details = action.details === undefined || sentTaskIds.has(action.id) ? undefined : action.details;\n\t\tawait this.webClient.chat.appendStream({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\tchunks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\tid: action.id,\n\t\t\t\t\ttitle: truncateSlackTaskText(action.title),\n\t\t\t\t\t...(details === undefined ? {} : { details: truncateSlackTaskText(details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t\tif (details !== undefined) {\n\t\t\tsentTaskIds.add(action.id);\n\t\t\tthis.taskDetailsSent.set(ref, sentTaskIds);\n\t\t}\n\t}\n\n\tasync stopStream(target: TransportOutputTarget, ref: string, result: TransportStreamResult): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tconst planChunks = this.planStreams.has(ref)\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"task_update\" as const,\n\t\t\t\t\t\tid: SLACK_RUN_TASK_ID,\n\t\t\t\t\t\ttitle: SLACK_RUN_TASK_TITLE,\n\t\t\t\t\t\tstatus: result.outcome,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"plan_update\" as const,\n\t\t\t\t\t\ttitle: result.outcome === \"complete\" ? SLACK_PLAN_COMPLETE_TITLE : SLACK_PLAN_ERROR_TITLE,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: [];\n\t\tconst payload = {\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\t// The stream starts in chunk mode for header/task updates, so final text\n\t\t\t// must use the same mode. Top-level markdown_text is rejected by Slack\n\t\t\t// with streaming_mode_mismatch after chunk-based starts.\n\t\t\tchunks: [...planChunks, { type: \"markdown_text\" as const, text: truncateSlackMarkdown(result.text) }],\n\t\t};\n\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t} catch {\n\t\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.taskDetailsSent.delete(ref);\n\t\t\tthis.planStreams.delete(ref);\n\t\t}\n\t}\n\n\tasync publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst materialized = await this.blobStore.materialize(artifact);\n\t\ttry {\n\t\t\tawait (this.webClient.files as any).uploadV2({\n\t\t\t\tchannel_id: target.channelId,\n\t\t\t\tthread_ts: target.threadId,\n\t\t\t\tfile: createReadStream(materialized.path),\n\t\t\t\tfilename: materialized.filename,\n\t\t\t\ttitle: artifact.title || artifact.name || materialized.filename,\n\t\t\t});\n\t\t} finally {\n\t\t\tawait materialized.cleanup?.();\n\t\t}\n\t}\n}\n"]}
|
package/dist/slack-sink.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createReadStream } from "fs";
|
|
2
2
|
export const SLACK_STREAM_HEADER = "Ich bearbeite jetzt deine Anfrage...";
|
|
3
|
+
export const SLACK_PLAN_ACTIVE_TITLE = "Biene Willy arbeitet 🐝";
|
|
4
|
+
export const SLACK_PLAN_COMPLETE_TITLE = "Biene Willy ist fertig 🍯";
|
|
5
|
+
export const SLACK_PLAN_ERROR_TITLE = "Biene Willy braucht Hilfe 🐝";
|
|
6
|
+
export const SLACK_RUN_TASK_ID = "bee-run";
|
|
7
|
+
export const SLACK_RUN_TASK_TITLE = "Anfrage bearbeiten";
|
|
3
8
|
export const SLACK_TASK_TEXT_LIMIT = 256;
|
|
4
9
|
export const SLACK_MARKDOWN_LIMIT = 12_000;
|
|
5
10
|
export const SLACK_MARKDOWN_TRUNCATION_SUFFIX = "\n\n_[Antwort auf 12.000 Zeichen gekürzt]_";
|
|
@@ -32,6 +37,8 @@ export class SlackSink {
|
|
|
32
37
|
this.blobStore = blobStore;
|
|
33
38
|
this.observer = observer;
|
|
34
39
|
}
|
|
40
|
+
taskDetailsSent = new Map();
|
|
41
|
+
planStreams = new Set();
|
|
35
42
|
async postMessage(target, text) {
|
|
36
43
|
if (!target.channelId) {
|
|
37
44
|
throw new Error("Missing Slack channel id");
|
|
@@ -73,6 +80,17 @@ export class SlackSink {
|
|
|
73
80
|
},
|
|
74
81
|
],
|
|
75
82
|
};
|
|
83
|
+
const planChunks = start.presentation === "plan"
|
|
84
|
+
? [
|
|
85
|
+
{ type: "plan_update", title: SLACK_PLAN_ACTIVE_TITLE },
|
|
86
|
+
{
|
|
87
|
+
type: "task_update",
|
|
88
|
+
id: SLACK_RUN_TASK_ID,
|
|
89
|
+
title: SLACK_RUN_TASK_TITLE,
|
|
90
|
+
status: "in_progress",
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
: [];
|
|
76
94
|
const result = await this.webClient.chat.startStream({
|
|
77
95
|
channel: target.channelId,
|
|
78
96
|
thread_ts: target.threadId,
|
|
@@ -80,15 +98,19 @@ export class SlackSink {
|
|
|
80
98
|
recipient_team_id: requiredContextString(start, "recipientTeamId"),
|
|
81
99
|
task_display_mode: start.presentation || "timeline",
|
|
82
100
|
// Slack supports block chunks although the current SDK's chunk union omits them.
|
|
83
|
-
chunks: [headerChunk],
|
|
101
|
+
chunks: [headerChunk, ...planChunks],
|
|
84
102
|
});
|
|
85
103
|
if (!result.ts)
|
|
86
104
|
throw new Error("Slack chat.startStream did not return a message timestamp");
|
|
105
|
+
if (start.presentation === "plan")
|
|
106
|
+
this.planStreams.add(result.ts);
|
|
87
107
|
return result.ts;
|
|
88
108
|
}
|
|
89
109
|
async updateStream(target, ref, action) {
|
|
90
110
|
if (!target.channelId)
|
|
91
111
|
throw new Error("Missing Slack channel id");
|
|
112
|
+
const sentTaskIds = this.taskDetailsSent.get(ref) ?? new Set();
|
|
113
|
+
const details = action.details === undefined || sentTaskIds.has(action.id) ? undefined : action.details;
|
|
92
114
|
await this.webClient.chat.appendStream({
|
|
93
115
|
channel: target.channelId,
|
|
94
116
|
ts: ref,
|
|
@@ -97,26 +119,53 @@ export class SlackSink {
|
|
|
97
119
|
type: "task_update",
|
|
98
120
|
id: action.id,
|
|
99
121
|
title: truncateSlackTaskText(action.title),
|
|
100
|
-
...(
|
|
122
|
+
...(details === undefined ? {} : { details: truncateSlackTaskText(details) }),
|
|
101
123
|
status: action.status,
|
|
102
124
|
},
|
|
103
125
|
],
|
|
104
126
|
});
|
|
127
|
+
if (details !== undefined) {
|
|
128
|
+
sentTaskIds.add(action.id);
|
|
129
|
+
this.taskDetailsSent.set(ref, sentTaskIds);
|
|
130
|
+
}
|
|
105
131
|
}
|
|
106
132
|
async stopStream(target, ref, result) {
|
|
107
133
|
if (!target.channelId)
|
|
108
134
|
throw new Error("Missing Slack channel id");
|
|
135
|
+
const planChunks = this.planStreams.has(ref)
|
|
136
|
+
? [
|
|
137
|
+
{
|
|
138
|
+
type: "task_update",
|
|
139
|
+
id: SLACK_RUN_TASK_ID,
|
|
140
|
+
title: SLACK_RUN_TASK_TITLE,
|
|
141
|
+
status: result.outcome,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: "plan_update",
|
|
145
|
+
title: result.outcome === "complete" ? SLACK_PLAN_COMPLETE_TITLE : SLACK_PLAN_ERROR_TITLE,
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
: [];
|
|
109
149
|
const payload = {
|
|
110
150
|
channel: target.channelId,
|
|
111
151
|
ts: ref,
|
|
112
|
-
|
|
152
|
+
// The stream starts in chunk mode for header/task updates, so final text
|
|
153
|
+
// must use the same mode. Top-level markdown_text is rejected by Slack
|
|
154
|
+
// with streaming_mode_mismatch after chunk-based starts.
|
|
155
|
+
chunks: [...planChunks, { type: "markdown_text", text: truncateSlackMarkdown(result.text) }],
|
|
113
156
|
};
|
|
114
157
|
try {
|
|
115
|
-
|
|
158
|
+
try {
|
|
159
|
+
await this.webClient.chat.stopStream(payload);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// One explicit retry keeps final-answer delivery resilient while remaining bounded.
|
|
163
|
+
await this.webClient.chat.stopStream(payload);
|
|
164
|
+
}
|
|
116
165
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
166
|
+
finally {
|
|
167
|
+
this.taskDetailsSent.delete(ref);
|
|
168
|
+
this.planStreams.delete(ref);
|
|
120
169
|
}
|
|
121
170
|
}
|
|
122
171
|
async publishArtifact(target, artifact) {
|
package/dist/slack-sink.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-sink.js","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tActionUpdate,\n\tArtifactRef,\n\tBlobStore,\n\tTransportOutputTarget,\n\tTransportSink,\n\tTransportStreamResult,\n\tTransportStreamStart,\n} from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport const SLACK_STREAM_HEADER = \"Ich bearbeite jetzt deine Anfrage...\";\nexport const SLACK_TASK_TEXT_LIMIT = 256;\nexport const SLACK_MARKDOWN_LIMIT = 12_000;\nexport const SLACK_MARKDOWN_TRUNCATION_SUFFIX = \"\\n\\n_[Antwort auf 12.000 Zeichen gekürzt]_\";\n\nfunction truncateCodePoints(text: string, limit: number, suffix: string): string {\n\tconst codePoints = Array.from(text);\n\tif (codePoints.length <= limit) return text;\n\tconst suffixCodePoints = Array.from(suffix);\n\treturn `${codePoints.slice(0, Math.max(0, limit - suffixCodePoints.length)).join(\"\")}${suffix}`;\n}\n\nexport function truncateSlackTaskText(text: string): string {\n\treturn truncateCodePoints(text, SLACK_TASK_TEXT_LIMIT, \"…\");\n}\n\nexport function truncateSlackMarkdown(text: string): string {\n\treturn truncateCodePoints(text, SLACK_MARKDOWN_LIMIT, SLACK_MARKDOWN_TRUNCATION_SUFFIX);\n}\n\nfunction requiredContextString(start: TransportStreamStart, key: string): string {\n\tconst value = start.context?.[key];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Missing Slack stream context ${key}`);\n\t}\n\treturn value;\n}\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\n\tasync postMessage(target: TransportOutputTarget, text: string): Promise<string> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\tchannel: target.channelId,\n\t\t\ttext,\n\t\t\tthread_ts: target.threadId,\n\t\t});\n\t\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\n\t}\n\n\tasync updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tawait this.webClient.chat.update({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\ttext,\n\t\t});\n\t\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tprepareStreamText(text: string): string {\n\t\treturn truncateSlackMarkdown(text);\n\t}\n\n\tasync startStream(target: TransportOutputTarget, start: TransportStreamStart): Promise<string> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tif (!target.threadId) throw new Error(\"Missing Slack stream thread timestamp\");\n\n\t\tconst headerChunk = {\n\t\t\ttype: \"blocks\",\n\t\t\tblocks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"header\",\n\t\t\t\t\ttext: { type: \"plain_text\", text: SLACK_STREAM_HEADER },\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst result = await this.webClient.chat.startStream({\n\t\t\tchannel: target.channelId,\n\t\t\tthread_ts: target.threadId,\n\t\t\trecipient_user_id: requiredContextString(start, \"recipientUserId\"),\n\t\t\trecipient_team_id: requiredContextString(start, \"recipientTeamId\"),\n\t\t\ttask_display_mode: start.presentation || \"timeline\",\n\t\t\t// Slack supports block chunks although the current SDK's chunk union omits them.\n\t\t\tchunks: [headerChunk] as never,\n\t\t});\n\t\tif (!result.ts) throw new Error(\"Slack chat.startStream did not return a message timestamp\");\n\t\treturn result.ts;\n\t}\n\n\tasync updateStream(target: TransportOutputTarget, ref: string, action: ActionUpdate): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\n\t\tawait this.webClient.chat.appendStream({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\tchunks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\tid: action.id,\n\t\t\t\t\ttitle: truncateSlackTaskText(action.title),\n\t\t\t\t\t...(action.details === undefined ? {} : { details: truncateSlackTaskText(action.details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t}\n\n\tasync stopStream(target: TransportOutputTarget, ref: string, result: TransportStreamResult): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tconst payload = {\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\tmarkdown_text: truncateSlackMarkdown(result.text),\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t} catch {\n\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t}\n\t}\n\n\tasync publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst materialized = await this.blobStore.materialize(artifact);\n\t\ttry {\n\t\t\tawait (this.webClient.files as any).uploadV2({\n\t\t\t\tchannel_id: target.channelId,\n\t\t\t\tthread_ts: target.threadId,\n\t\t\t\tfile: createReadStream(materialized.path),\n\t\t\t\tfilename: materialized.filename,\n\t\t\t\ttitle: artifact.title || artifact.name || materialized.filename,\n\t\t\t});\n\t\t} finally {\n\t\t\tawait materialized.cleanup?.();\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"slack-sink.js","sourceRoot":"","sources":["../src/slack-sink.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;AAEtC,MAAM,CAAC,MAAM,mBAAmB,GAAG,sCAAsC,CAAC;AAC1E,MAAM,CAAC,MAAM,uBAAuB,GAAG,2BAAwB,CAAC;AAChE,MAAM,CAAC,MAAM,yBAAyB,GAAG,6BAA0B,CAAC;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAG,gCAA6B,CAAC;AACpE,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAC3C,MAAM,CAAC,MAAM,oBAAoB,GAAG,oBAAoB,CAAC;AACzD,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AACzC,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAC3C,MAAM,CAAC,MAAM,gCAAgC,GAAG,6CAA4C,CAAC;AAE7F,SAAS,kBAAkB,CAAC,IAAY,EAAE,KAAa,EAAE,MAAc,EAAU;IAChF,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,UAAU,CAAC,MAAM,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,gBAAgB,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5C,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;AAAA,CAChG;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAU;IAC3D,OAAO,kBAAkB,CAAC,IAAI,EAAE,qBAAqB,EAAE,KAAG,CAAC,CAAC;AAAA,CAC5D;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAU;IAC3D,OAAO,kBAAkB,CAAC,IAAI,EAAE,oBAAoB,EAAE,gCAAgC,CAAC,CAAC;AAAA,CACxF;AAED,SAAS,qBAAqB,CAAC,KAA2B,EAAE,GAAW,EAAU;IAChF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAOD,MAAM,OAAO,SAAS;IAEZ,SAAS;IACT,SAAS;IACT,QAAQ;IAHjB,YACS,SAAoB,EACpB,SAAoB,EACpB,QAA4B,EACnC;yBAHO,SAAS;yBACT,SAAS;wBACT,QAAQ;IACd,CAAC;IACa,eAAe,GAAG,IAAI,GAAG,EAAuB,CAAC;IACjD,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEjD,KAAK,CAAC,WAAW,CAAC,MAA6B,EAAE,IAAY,EAAmB;QAC/E,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;YACpD,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,IAAI;YACJ,SAAS,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,EAAY,CAAC;QAChC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACzC,OAAO,GAAG,CAAC;IAAA,CACX;IAED,KAAK,CAAC,aAAa,CAAC,MAA6B,EAAE,GAAW,EAAE,IAAY,EAAiB;QAC5F,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;YAChC,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,EAAE,EAAE,GAAG;YACP,IAAI;SACJ,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAAA,CAC1C;IAED,iBAAiB,CAAC,IAAY,EAAU;QACvC,OAAO,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAAA,CACnC;IAED,KAAK,CAAC,WAAW,CAAC,MAA6B,EAAE,KAA2B,EAAmB;QAC9F,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAE/E,MAAM,WAAW,GAAG;YACnB,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE;gBACP;oBACC,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,mBAAmB,EAAE;iBACvD;aACD;SACD,CAAC;QACF,MAAM,UAAU,GACf,KAAK,CAAC,YAAY,KAAK,MAAM;YAC5B,CAAC,CAAC;gBACA,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,uBAAuB,EAAE;gBACvD;oBACC,IAAI,EAAE,aAAa;oBACnB,EAAE,EAAE,iBAAiB;oBACrB,KAAK,EAAE,oBAAoB;oBAC3B,MAAM,EAAE,aAAa;iBACrB;aACD;YACF,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;YACpD,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,SAAS,EAAE,MAAM,CAAC,QAAQ;YAC1B,iBAAiB,EAAE,qBAAqB,CAAC,KAAK,EAAE,iBAAiB,CAAC;YAClE,iBAAiB,EAAE,qBAAqB,CAAC,KAAK,EAAE,iBAAiB,CAAC;YAClE,iBAAiB,EAAE,KAAK,CAAC,YAAY,IAAI,UAAU;YACnD,iFAAiF;YACjF,MAAM,EAAE,CAAC,WAAW,EAAE,GAAG,UAAU,CAAU;SAC7C,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;QAC7F,IAAI,KAAK,CAAC,YAAY,KAAK,MAAM;YAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACnE,OAAO,MAAM,CAAC,EAAE,CAAC;IAAA,CACjB;IAED,KAAK,CAAC,YAAY,CAAC,MAA6B,EAAE,GAAW,EAAE,MAAoB,EAAiB;QACnG,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAEnE,MAAM,WAAW,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,EAAU,CAAC;QACvE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;QACxG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC;YACtC,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,EAAE,EAAE,GAAG;YACP,MAAM,EAAE;gBACP;oBACC,IAAI,EAAE,aAAa;oBACnB,EAAE,EAAE,MAAM,CAAC,EAAE;oBACb,KAAK,EAAE,qBAAqB,CAAC,MAAM,CAAC,KAAK,CAAC;oBAC1C,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC7E,MAAM,EAAE,MAAM,CAAC,MAAM;iBACrB;aACD;SACD,CAAC,CAAC;QACH,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC3B,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAC5C,CAAC;IAAA,CACD;IAED,KAAK,CAAC,UAAU,CAAC,MAA6B,EAAE,GAAW,EAAE,MAA6B,EAAiB;QAC1G,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QACnE,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;YAC3C,CAAC,CAAC;gBACA;oBACC,IAAI,EAAE,aAAsB;oBAC5B,EAAE,EAAE,iBAAiB;oBACrB,KAAK,EAAE,oBAAoB;oBAC3B,MAAM,EAAE,MAAM,CAAC,OAAO;iBACtB;gBACD;oBACC,IAAI,EAAE,aAAsB;oBAC5B,KAAK,EAAE,MAAM,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,sBAAsB;iBACzF;aACD;YACF,CAAC,CAAC,EAAE,CAAC;QACN,MAAM,OAAO,GAAG;YACf,OAAO,EAAE,MAAM,CAAC,SAAS;YACzB,EAAE,EAAE,GAAG;YACP,yEAAyE;YACzE,uEAAuE;YACvE,yDAAyD;YACzD,MAAM,EAAE,CAAC,GAAG,UAAU,EAAE,EAAE,IAAI,EAAE,eAAwB,EAAE,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;SACrG,CAAC;QAEF,IAAI,CAAC;YACJ,IAAI,CAAC;gBACJ,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC/C,CAAC;YAAC,MAAM,CAAC;gBACR,oFAAoF;gBACpF,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC/C,CAAC;QACF,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;IAAA,CACD;IAED,KAAK,CAAC,eAAe,CAAC,MAA6B,EAAE,QAAqB,EAAiB;QAC1F,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC7C,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAChE,IAAI,CAAC;YACJ,MAAO,IAAI,CAAC,SAAS,CAAC,KAAa,CAAC,QAAQ,CAAC;gBAC5C,UAAU,EAAE,MAAM,CAAC,SAAS;gBAC5B,SAAS,EAAE,MAAM,CAAC,QAAQ;gBAC1B,IAAI,EAAE,gBAAgB,CAAC,YAAY,CAAC,IAAI,CAAC;gBACzC,QAAQ,EAAE,YAAY,CAAC,QAAQ;gBAC/B,KAAK,EAAE,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,IAAI,IAAI,YAAY,CAAC,QAAQ;aAC/D,CAAC,CAAC;QACJ,CAAC;gBAAS,CAAC;YACV,MAAM,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC;QAChC,CAAC;IAAA,CACD;CACD","sourcesContent":["import type {\n\tActionUpdate,\n\tArtifactRef,\n\tBlobStore,\n\tTransportOutputTarget,\n\tTransportSink,\n\tTransportStreamResult,\n\tTransportStreamStart,\n} from \"@jobmatchme/bee-gate\";\nimport type { WebClient } from \"@slack/web-api\";\nimport { createReadStream } from \"fs\";\n\nexport const SLACK_STREAM_HEADER = \"Ich bearbeite jetzt deine Anfrage...\";\nexport const SLACK_PLAN_ACTIVE_TITLE = \"Biene Willy arbeitet 🐝\";\nexport const SLACK_PLAN_COMPLETE_TITLE = \"Biene Willy ist fertig 🍯\";\nexport const SLACK_PLAN_ERROR_TITLE = \"Biene Willy braucht Hilfe 🐝\";\nexport const SLACK_RUN_TASK_ID = \"bee-run\";\nexport const SLACK_RUN_TASK_TITLE = \"Anfrage bearbeiten\";\nexport const SLACK_TASK_TEXT_LIMIT = 256;\nexport const SLACK_MARKDOWN_LIMIT = 12_000;\nexport const SLACK_MARKDOWN_TRUNCATION_SUFFIX = \"\\n\\n_[Antwort auf 12.000 Zeichen gekürzt]_\";\n\nfunction truncateCodePoints(text: string, limit: number, suffix: string): string {\n\tconst codePoints = Array.from(text);\n\tif (codePoints.length <= limit) return text;\n\tconst suffixCodePoints = Array.from(suffix);\n\treturn `${codePoints.slice(0, Math.max(0, limit - suffixCodePoints.length)).join(\"\")}${suffix}`;\n}\n\nexport function truncateSlackTaskText(text: string): string {\n\treturn truncateCodePoints(text, SLACK_TASK_TEXT_LIMIT, \"…\");\n}\n\nexport function truncateSlackMarkdown(text: string): string {\n\treturn truncateCodePoints(text, SLACK_MARKDOWN_LIMIT, SLACK_MARKDOWN_TRUNCATION_SUFFIX);\n}\n\nfunction requiredContextString(start: TransportStreamStart, key: string): string {\n\tconst value = start.context?.[key];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Missing Slack stream context ${key}`);\n\t}\n\treturn value;\n}\n\nexport interface SlackSinkObserver {\n\tposted(target: TransportOutputTarget, ref: string, text: string): void;\n\tupdated(target: TransportOutputTarget, ref: string, text: string): void;\n}\n\nexport class SlackSink implements TransportSink<string> {\n\tconstructor(\n\t\tprivate webClient: WebClient,\n\t\tprivate blobStore: BlobStore,\n\t\tprivate observer?: SlackSinkObserver,\n\t) {}\n\tprivate readonly taskDetailsSent = new Map<string, Set<string>>();\n\tprivate readonly planStreams = new Set<string>();\n\n\tasync postMessage(target: TransportOutputTarget, text: string): Promise<string> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\tchannel: target.channelId,\n\t\t\ttext,\n\t\t\tthread_ts: target.threadId,\n\t\t});\n\t\tconst ref = result.ts as string;\n\t\tthis.observer?.posted(target, ref, text);\n\t\treturn ref;\n\t}\n\n\tasync updateMessage(target: TransportOutputTarget, ref: string, text: string): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tawait this.webClient.chat.update({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\ttext,\n\t\t});\n\t\tthis.observer?.updated(target, ref, text);\n\t}\n\n\tprepareStreamText(text: string): string {\n\t\treturn truncateSlackMarkdown(text);\n\t}\n\n\tasync startStream(target: TransportOutputTarget, start: TransportStreamStart): Promise<string> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tif (!target.threadId) throw new Error(\"Missing Slack stream thread timestamp\");\n\n\t\tconst headerChunk = {\n\t\t\ttype: \"blocks\",\n\t\t\tblocks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"header\",\n\t\t\t\t\ttext: { type: \"plain_text\", text: SLACK_STREAM_HEADER },\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t\tconst planChunks =\n\t\t\tstart.presentation === \"plan\"\n\t\t\t\t? [\n\t\t\t\t\t\t{ type: \"plan_update\", title: SLACK_PLAN_ACTIVE_TITLE },\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\t\t\tid: SLACK_RUN_TASK_ID,\n\t\t\t\t\t\t\ttitle: SLACK_RUN_TASK_TITLE,\n\t\t\t\t\t\t\tstatus: \"in_progress\",\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [];\n\t\tconst result = await this.webClient.chat.startStream({\n\t\t\tchannel: target.channelId,\n\t\t\tthread_ts: target.threadId,\n\t\t\trecipient_user_id: requiredContextString(start, \"recipientUserId\"),\n\t\t\trecipient_team_id: requiredContextString(start, \"recipientTeamId\"),\n\t\t\ttask_display_mode: start.presentation || \"timeline\",\n\t\t\t// Slack supports block chunks although the current SDK's chunk union omits them.\n\t\t\tchunks: [headerChunk, ...planChunks] as never,\n\t\t});\n\t\tif (!result.ts) throw new Error(\"Slack chat.startStream did not return a message timestamp\");\n\t\tif (start.presentation === \"plan\") this.planStreams.add(result.ts);\n\t\treturn result.ts;\n\t}\n\n\tasync updateStream(target: TransportOutputTarget, ref: string, action: ActionUpdate): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\n\t\tconst sentTaskIds = this.taskDetailsSent.get(ref) ?? new Set<string>();\n\t\tconst details = action.details === undefined || sentTaskIds.has(action.id) ? undefined : action.details;\n\t\tawait this.webClient.chat.appendStream({\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\tchunks: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"task_update\",\n\t\t\t\t\tid: action.id,\n\t\t\t\t\ttitle: truncateSlackTaskText(action.title),\n\t\t\t\t\t...(details === undefined ? {} : { details: truncateSlackTaskText(details) }),\n\t\t\t\t\tstatus: action.status,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t\tif (details !== undefined) {\n\t\t\tsentTaskIds.add(action.id);\n\t\t\tthis.taskDetailsSent.set(ref, sentTaskIds);\n\t\t}\n\t}\n\n\tasync stopStream(target: TransportOutputTarget, ref: string, result: TransportStreamResult): Promise<void> {\n\t\tif (!target.channelId) throw new Error(\"Missing Slack channel id\");\n\t\tconst planChunks = this.planStreams.has(ref)\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"task_update\" as const,\n\t\t\t\t\t\tid: SLACK_RUN_TASK_ID,\n\t\t\t\t\t\ttitle: SLACK_RUN_TASK_TITLE,\n\t\t\t\t\t\tstatus: result.outcome,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"plan_update\" as const,\n\t\t\t\t\t\ttitle: result.outcome === \"complete\" ? SLACK_PLAN_COMPLETE_TITLE : SLACK_PLAN_ERROR_TITLE,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: [];\n\t\tconst payload = {\n\t\t\tchannel: target.channelId,\n\t\t\tts: ref,\n\t\t\t// The stream starts in chunk mode for header/task updates, so final text\n\t\t\t// must use the same mode. Top-level markdown_text is rejected by Slack\n\t\t\t// with streaming_mode_mismatch after chunk-based starts.\n\t\t\tchunks: [...planChunks, { type: \"markdown_text\" as const, text: truncateSlackMarkdown(result.text) }],\n\t\t};\n\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t} catch {\n\t\t\t\t// One explicit retry keeps final-answer delivery resilient while remaining bounded.\n\t\t\t\tawait this.webClient.chat.stopStream(payload);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.taskDetailsSent.delete(ref);\n\t\t\tthis.planStreams.delete(ref);\n\t\t}\n\t}\n\n\tasync publishArtifact(target: TransportOutputTarget, artifact: ArtifactRef): Promise<void> {\n\t\tif (!target.channelId) {\n\t\t\tthrow new Error(\"Missing Slack channel id\");\n\t\t}\n\n\t\tconst materialized = await this.blobStore.materialize(artifact);\n\t\ttry {\n\t\t\tawait (this.webClient.files as any).uploadV2({\n\t\t\t\tchannel_id: target.channelId,\n\t\t\t\tthread_ts: target.threadId,\n\t\t\t\tfile: createReadStream(materialized.path),\n\t\t\t\tfilename: materialized.filename,\n\t\t\t\ttitle: artifact.title || artifact.name || materialized.filename,\n\t\t\t});\n\t\t} finally {\n\t\t\tawait materialized.cleanup?.();\n\t\t}\n\t}\n}\n"]}
|