@inkeep/agents-work-apps 0.0.0-dev-20260212220816 → 0.0.0-dev-20260213181729

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.
@@ -2,6 +2,7 @@ import { env } from "../../../env.js";
2
2
  import { getLogger } from "../../../logger.js";
3
3
  import { createContextBlock } from "../blocks/index.js";
4
4
  import { SlackErrorType, classifyError, getUserFriendlyErrorMessage } from "./utils.js";
5
+ import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
5
6
 
6
7
  //#region src/slack/services/events/streaming.ts
7
8
  /**
@@ -28,46 +29,87 @@ async function withTimeout(promise, ms, label) {
28
29
  }
29
30
  }
30
31
  async function streamAgentResponse(params) {
31
- const { slackClient, channel, threadTs, thinkingMessageTs, slackUserId, teamId, jwtToken, projectId, agentId, question, agentName, conversationId } = params;
32
- const apiUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
33
- logger.debug({
34
- conversationId,
35
- channel,
36
- threadTs
37
- }, "Streaming agent response with conversation context");
38
- const abortController = new AbortController();
39
- const timeoutId = setTimeout(() => {
40
- logger.warn({
32
+ return tracer.startActiveSpan(SLACK_SPAN_NAMES.STREAM_AGENT_RESPONSE, async (span) => {
33
+ const { slackClient, channel, threadTs, thinkingMessageTs, slackUserId, teamId, jwtToken, projectId, agentId, question, agentName, conversationId } = params;
34
+ span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
35
+ span.setAttribute(SLACK_SPAN_KEYS.CHANNEL_ID, channel);
36
+ span.setAttribute(SLACK_SPAN_KEYS.USER_ID, slackUserId);
37
+ span.setAttribute(SLACK_SPAN_KEYS.PROJECT_ID, projectId);
38
+ span.setAttribute(SLACK_SPAN_KEYS.AGENT_ID, agentId);
39
+ if (conversationId) span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId);
40
+ span.setAttribute(SLACK_SPAN_KEYS.THREAD_TS, threadTs);
41
+ const apiUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
42
+ logger.info({
43
+ conversationId,
41
44
  channel,
42
45
  threadTs,
43
- timeoutMs: STREAM_TIMEOUT_MS
44
- }, "Stream timeout reached");
45
- abortController.abort();
46
- }, STREAM_TIMEOUT_MS);
47
- let response;
48
- try {
49
- response = await fetch(`${apiUrl.replace(/\/$/, "")}/run/api/chat`, {
50
- method: "POST",
51
- headers: {
52
- "Content-Type": "application/json",
53
- Authorization: `Bearer ${jwtToken}`,
54
- "x-inkeep-project-id": projectId,
55
- "x-inkeep-agent-id": agentId
56
- },
57
- body: JSON.stringify({
58
- messages: [{
59
- role: "user",
60
- content: question
61
- }],
62
- stream: true,
63
- ...conversationId && { conversationId }
64
- }),
65
- signal: abortController.signal
66
- });
67
- } catch (fetchError) {
68
- clearTimeout(timeoutId);
69
- if (fetchError.name === "AbortError") {
70
- const errorType = SlackErrorType.TIMEOUT;
46
+ agentId,
47
+ projectId
48
+ }, "Starting streaming agent response");
49
+ const abortController = new AbortController();
50
+ const timeoutId = setTimeout(() => {
51
+ logger.warn({
52
+ channel,
53
+ threadTs,
54
+ timeoutMs: STREAM_TIMEOUT_MS
55
+ }, "Stream timeout reached");
56
+ abortController.abort();
57
+ }, STREAM_TIMEOUT_MS);
58
+ let response;
59
+ try {
60
+ response = await fetch(`${apiUrl.replace(/\/$/, "")}/run/api/chat`, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ Authorization: `Bearer ${jwtToken}`,
65
+ "x-inkeep-project-id": projectId,
66
+ "x-inkeep-agent-id": agentId
67
+ },
68
+ body: JSON.stringify({
69
+ messages: [{
70
+ role: "user",
71
+ content: question
72
+ }],
73
+ stream: true,
74
+ ...conversationId && { conversationId }
75
+ }),
76
+ signal: abortController.signal
77
+ });
78
+ } catch (fetchError) {
79
+ clearTimeout(timeoutId);
80
+ if (fetchError.name === "AbortError") {
81
+ const errorType = SlackErrorType.TIMEOUT;
82
+ const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
83
+ await slackClient.chat.postMessage({
84
+ channel,
85
+ thread_ts: threadTs,
86
+ text: errorMessage
87
+ });
88
+ if (thinkingMessageTs) try {
89
+ await slackClient.chat.delete({
90
+ channel,
91
+ ts: thinkingMessageTs
92
+ });
93
+ } catch {}
94
+ span.end();
95
+ return {
96
+ success: false,
97
+ errorType,
98
+ errorMessage
99
+ };
100
+ }
101
+ if (fetchError instanceof Error) setSpanWithError(span, fetchError);
102
+ span.end();
103
+ throw fetchError;
104
+ }
105
+ if (!response.ok) {
106
+ clearTimeout(timeoutId);
107
+ const errorBody = await response.text().catch(() => "Unknown error");
108
+ logger.error({
109
+ status: response.status,
110
+ errorBody
111
+ }, "Agent streaming request failed");
112
+ const errorType = classifyError(null, response.status);
71
113
  const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
72
114
  await slackClient.chat.postMessage({
73
115
  channel,
@@ -80,153 +122,133 @@ async function streamAgentResponse(params) {
80
122
  ts: thinkingMessageTs
81
123
  });
82
124
  } catch {}
125
+ span.end();
83
126
  return {
84
127
  success: false,
85
128
  errorType,
86
129
  errorMessage
87
130
  };
88
131
  }
89
- throw fetchError;
90
- }
91
- if (!response.ok) {
92
- clearTimeout(timeoutId);
93
- const errorBody = await response.text().catch(() => "Unknown error");
94
- logger.error({
95
- status: response.status,
96
- errorBody
97
- }, "Agent streaming request failed");
98
- const errorType = classifyError(null, response.status);
99
- const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
100
- await slackClient.chat.postMessage({
101
- channel,
102
- thread_ts: threadTs,
103
- text: errorMessage
104
- });
105
- if (thinkingMessageTs) try {
106
- await slackClient.chat.delete({
107
- channel,
108
- ts: thinkingMessageTs
109
- });
110
- } catch {}
111
- return {
112
- success: false,
113
- errorType,
114
- errorMessage
115
- };
116
- }
117
- if (!response.body) {
118
- clearTimeout(timeoutId);
119
- logger.error({
120
- status: response.status,
121
- channel,
122
- threadTs
123
- }, "Agent API returned 200 but no response body");
124
- const errorType = SlackErrorType.API_ERROR;
125
- const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
126
- await slackClient.chat.postMessage({
127
- channel,
128
- thread_ts: threadTs,
129
- text: errorMessage
130
- });
131
- if (thinkingMessageTs) try {
132
- await slackClient.chat.delete({
133
- channel,
134
- ts: thinkingMessageTs
135
- });
136
- } catch {}
137
- return {
138
- success: false,
139
- errorType,
140
- errorMessage
141
- };
142
- }
143
- const reader = response.body.getReader();
144
- const decoder = new TextDecoder();
145
- let buffer = "";
146
- let fullText = "";
147
- const streamer = slackClient.chatStream({
148
- channel,
149
- recipient_team_id: teamId,
150
- recipient_user_id: slackUserId,
151
- thread_ts: threadTs
152
- });
153
- try {
154
- while (true) {
155
- const { done, value } = await reader.read();
156
- if (done) break;
157
- buffer += decoder.decode(value, { stream: true });
158
- const lines = buffer.split("\n");
159
- buffer = lines.pop() || "";
160
- for (const line of lines) {
161
- if (!line.startsWith("data: ")) continue;
162
- const jsonStr = line.slice(6).trim();
163
- if (!jsonStr || jsonStr === "[DONE]") continue;
164
- try {
165
- const data = JSON.parse(jsonStr);
166
- if (data.type === "data-operation") continue;
167
- if (data.type === "text-start" || data.type === "text-end") continue;
168
- if (data.type === "text-delta" && data.delta) {
169
- fullText += data.delta;
170
- await withTimeout(streamer.append({ markdown_text: data.delta }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.append");
171
- } else if (data.object === "chat.completion.chunk" && data.choices?.[0]?.delta?.content) {
172
- const content = data.choices[0].delta.content;
173
- try {
174
- if (JSON.parse(content).type === "data-operation") continue;
175
- } catch {}
176
- fullText += content;
177
- await withTimeout(streamer.append({ markdown_text: content }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.append");
178
- }
179
- } catch {}
180
- }
181
- }
182
- clearTimeout(timeoutId);
183
- const contextBlock = createContextBlock({ agentName });
184
- await withTimeout(streamer.stop({ blocks: [contextBlock] }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.stop");
185
- if (thinkingMessageTs) try {
186
- await slackClient.chat.delete({
187
- channel,
188
- ts: thinkingMessageTs
189
- });
190
- } catch (deleteError) {
191
- logger.warn({ deleteError }, "Failed to delete acknowledgement message");
192
- }
193
- logger.debug({
194
- channel,
195
- threadTs,
196
- responseLength: fullText.length
197
- }, "Streaming completed");
198
- return { success: true };
199
- } catch (streamError) {
200
- clearTimeout(timeoutId);
201
- logger.error({ streamError }, "Error during Slack streaming");
202
- await withTimeout(streamer.stop(), CHATSTREAM_OP_TIMEOUT_MS, "streamer.stop").catch((e) => logger.warn({ error: e }, "Failed to stop streamer during error cleanup"));
203
- if (thinkingMessageTs) try {
204
- await slackClient.chat.delete({
132
+ if (!response.body) {
133
+ clearTimeout(timeoutId);
134
+ logger.error({
135
+ status: response.status,
205
136
  channel,
206
- ts: thinkingMessageTs
207
- });
208
- } catch {}
209
- const errorType = classifyError(streamError);
210
- const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
211
- try {
137
+ threadTs
138
+ }, "Agent API returned 200 but no response body");
139
+ const errorType = SlackErrorType.API_ERROR;
140
+ const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
212
141
  await slackClient.chat.postMessage({
213
142
  channel,
214
143
  thread_ts: threadTs,
215
144
  text: errorMessage
216
145
  });
217
- } catch (notifyError) {
218
- logger.warn({
219
- notifyError,
146
+ if (thinkingMessageTs) try {
147
+ await slackClient.chat.delete({
148
+ channel,
149
+ ts: thinkingMessageTs
150
+ });
151
+ } catch {}
152
+ span.end();
153
+ return {
154
+ success: false,
155
+ errorType,
156
+ errorMessage
157
+ };
158
+ }
159
+ const reader = response.body.getReader();
160
+ const decoder = new TextDecoder();
161
+ let buffer = "";
162
+ let fullText = "";
163
+ const streamer = slackClient.chatStream({
164
+ channel,
165
+ recipient_team_id: teamId,
166
+ recipient_user_id: slackUserId,
167
+ thread_ts: threadTs
168
+ });
169
+ try {
170
+ while (true) {
171
+ const { done, value } = await reader.read();
172
+ if (done) break;
173
+ buffer += decoder.decode(value, { stream: true });
174
+ const lines = buffer.split("\n");
175
+ buffer = lines.pop() || "";
176
+ for (const line of lines) {
177
+ if (!line.startsWith("data: ")) continue;
178
+ const jsonStr = line.slice(6).trim();
179
+ if (!jsonStr || jsonStr === "[DONE]") continue;
180
+ try {
181
+ const data = JSON.parse(jsonStr);
182
+ if (data.type === "data-operation") continue;
183
+ if (data.type === "text-start" || data.type === "text-end") continue;
184
+ if (data.type === "text-delta" && data.delta) {
185
+ fullText += data.delta;
186
+ await withTimeout(streamer.append({ markdown_text: data.delta }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.append");
187
+ } else if (data.object === "chat.completion.chunk" && data.choices?.[0]?.delta?.content) {
188
+ const content = data.choices[0].delta.content;
189
+ try {
190
+ if (JSON.parse(content).type === "data-operation") continue;
191
+ } catch {}
192
+ fullText += content;
193
+ await withTimeout(streamer.append({ markdown_text: content }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.append");
194
+ }
195
+ } catch {}
196
+ }
197
+ }
198
+ clearTimeout(timeoutId);
199
+ const contextBlock = createContextBlock({ agentName });
200
+ await withTimeout(streamer.stop({ blocks: [contextBlock] }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.stop");
201
+ if (thinkingMessageTs) try {
202
+ await slackClient.chat.delete({
203
+ channel,
204
+ ts: thinkingMessageTs
205
+ });
206
+ } catch (deleteError) {
207
+ logger.warn({ deleteError }, "Failed to delete acknowledgement message");
208
+ }
209
+ logger.info({
220
210
  channel,
221
- threadTs
222
- }, "Failed to notify user of stream error");
211
+ threadTs,
212
+ responseLength: fullText.length,
213
+ agentId,
214
+ conversationId
215
+ }, "Streaming completed");
216
+ span.end();
217
+ return { success: true };
218
+ } catch (streamError) {
219
+ clearTimeout(timeoutId);
220
+ if (streamError instanceof Error) setSpanWithError(span, streamError);
221
+ logger.error({ streamError }, "Error during Slack streaming");
222
+ await withTimeout(streamer.stop(), CHATSTREAM_OP_TIMEOUT_MS, "streamer.stop").catch((e) => logger.warn({ error: e }, "Failed to stop streamer during error cleanup"));
223
+ if (thinkingMessageTs) try {
224
+ await slackClient.chat.delete({
225
+ channel,
226
+ ts: thinkingMessageTs
227
+ });
228
+ } catch {}
229
+ const errorType = classifyError(streamError);
230
+ const errorMessage = getUserFriendlyErrorMessage(errorType, agentName);
231
+ try {
232
+ await slackClient.chat.postMessage({
233
+ channel,
234
+ thread_ts: threadTs,
235
+ text: errorMessage
236
+ });
237
+ } catch (notifyError) {
238
+ logger.warn({
239
+ notifyError,
240
+ channel,
241
+ threadTs
242
+ }, "Failed to notify user of stream error");
243
+ }
244
+ span.end();
245
+ return {
246
+ success: false,
247
+ errorType,
248
+ errorMessage
249
+ };
223
250
  }
224
- return {
225
- success: false,
226
- errorType,
227
- errorMessage
228
- };
229
- }
251
+ });
230
252
  }
231
253
 
232
254
  //#endregion
@@ -0,0 +1,40 @@
1
+ import { setSpanWithError } from "@inkeep/agents-core";
2
+ import * as _opentelemetry_api0 from "@opentelemetry/api";
3
+
4
+ //#region src/slack/tracer.d.ts
5
+ declare const tracer: _opentelemetry_api0.Tracer;
6
+ declare const SLACK_SPAN_NAMES: {
7
+ readonly WEBHOOK: "slack.webhook";
8
+ readonly APP_MENTION: "slack.app_mention";
9
+ readonly BLOCK_ACTION: "slack.block_action";
10
+ readonly MODAL_SUBMISSION: "slack.modal_submission";
11
+ readonly FOLLOW_UP_SUBMISSION: "slack.follow_up_submission";
12
+ readonly MESSAGE_SHORTCUT: "slack.message_shortcut";
13
+ readonly STREAM_AGENT_RESPONSE: "slack.stream_agent_response";
14
+ readonly OPEN_AGENT_SELECTOR_MODAL: "slack.open_agent_selector_modal";
15
+ readonly OPEN_FOLLOW_UP_MODAL: "slack.open_follow_up_modal";
16
+ readonly PROJECT_SELECT_UPDATE: "slack.project_select_update";
17
+ readonly CALL_AGENT_API: "slack.call_agent_api";
18
+ };
19
+ declare const SLACK_SPAN_KEYS: {
20
+ readonly TEAM_ID: "slack.team_id";
21
+ readonly CHANNEL_ID: "slack.channel_id";
22
+ readonly USER_ID: "slack.user_id";
23
+ readonly EVENT_TYPE: "slack.event_type";
24
+ readonly INNER_EVENT_TYPE: "slack.inner_event_type";
25
+ readonly CALLBACK_ID: "slack.callback_id";
26
+ readonly ACTION_IDS: "slack.action_ids";
27
+ readonly THREAD_TS: "slack.thread_ts";
28
+ readonly MESSAGE_TS: "slack.message_ts";
29
+ readonly TENANT_ID: "slack.tenant_id";
30
+ readonly PROJECT_ID: "slack.project_id";
31
+ readonly AGENT_ID: "slack.agent_id";
32
+ readonly CONVERSATION_ID: "slack.conversation_id";
33
+ readonly OUTCOME: "slack.outcome";
34
+ readonly IS_BOT_MESSAGE: "slack.is_bot_message";
35
+ readonly HAS_QUERY: "slack.has_query";
36
+ readonly IS_IN_THREAD: "slack.is_in_thread";
37
+ };
38
+ type SlackOutcome = 'handled' | 'ignored_bot_message' | 'ignored_unknown_event' | 'ignored_no_action_match' | 'url_verification' | 'validation_error' | 'signature_invalid' | 'error';
39
+ //#endregion
40
+ export { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, SlackOutcome, setSpanWithError, tracer };
@@ -0,0 +1,39 @@
1
+ import { getTracer, setSpanWithError } from "@inkeep/agents-core";
2
+
3
+ //#region src/slack/tracer.ts
4
+ const tracer = getTracer("agents-work-apps-slack");
5
+ const SLACK_SPAN_NAMES = {
6
+ WEBHOOK: "slack.webhook",
7
+ APP_MENTION: "slack.app_mention",
8
+ BLOCK_ACTION: "slack.block_action",
9
+ MODAL_SUBMISSION: "slack.modal_submission",
10
+ FOLLOW_UP_SUBMISSION: "slack.follow_up_submission",
11
+ MESSAGE_SHORTCUT: "slack.message_shortcut",
12
+ STREAM_AGENT_RESPONSE: "slack.stream_agent_response",
13
+ OPEN_AGENT_SELECTOR_MODAL: "slack.open_agent_selector_modal",
14
+ OPEN_FOLLOW_UP_MODAL: "slack.open_follow_up_modal",
15
+ PROJECT_SELECT_UPDATE: "slack.project_select_update",
16
+ CALL_AGENT_API: "slack.call_agent_api"
17
+ };
18
+ const SLACK_SPAN_KEYS = {
19
+ TEAM_ID: "slack.team_id",
20
+ CHANNEL_ID: "slack.channel_id",
21
+ USER_ID: "slack.user_id",
22
+ EVENT_TYPE: "slack.event_type",
23
+ INNER_EVENT_TYPE: "slack.inner_event_type",
24
+ CALLBACK_ID: "slack.callback_id",
25
+ ACTION_IDS: "slack.action_ids",
26
+ THREAD_TS: "slack.thread_ts",
27
+ MESSAGE_TS: "slack.message_ts",
28
+ TENANT_ID: "slack.tenant_id",
29
+ PROJECT_ID: "slack.project_id",
30
+ AGENT_ID: "slack.agent_id",
31
+ CONVERSATION_ID: "slack.conversation_id",
32
+ OUTCOME: "slack.outcome",
33
+ IS_BOT_MESSAGE: "slack.is_bot_message",
34
+ HAS_QUERY: "slack.has_query",
35
+ IS_IN_THREAD: "slack.is_in_thread"
36
+ };
37
+
38
+ //#endregion
39
+ export { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/agents-work-apps",
3
- "version": "0.0.0-dev-20260212220816",
3
+ "version": "0.0.0-dev-20260213181729",
4
4
  "description": "First party integrations for Inkeep Agents",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -24,15 +24,16 @@
24
24
  "@nangohq/node": "^0.69.20",
25
25
  "@octokit/auth-app": "^8.1.2",
26
26
  "@octokit/rest": "^22.0.1",
27
+ "@opentelemetry/api": "^1.9.0",
27
28
  "@slack/types": "^2.18.0",
28
29
  "@slack/web-api": "^7.9.1",
29
- "slack-block-builder": "^2.8.0",
30
30
  "drizzle-orm": "^0.44.7",
31
31
  "fetch-to-node": "^2.1.0",
32
32
  "hono": "^4.11.7",
33
33
  "jose": "^6.1.0",
34
34
  "minimatch": "^10.1.1",
35
- "@inkeep/agents-core": "0.0.0-dev-20260212220816"
35
+ "slack-block-builder": "^2.8.0",
36
+ "@inkeep/agents-core": "0.0.0-dev-20260213181729"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "@hono/zod-openapi": "^1.1.5",