@lota-sdk/core 0.1.16 → 0.1.17
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/package.json +6 -3
- package/src/ai/definitions.ts +1 -1
- package/src/ai/embedding-cache.ts +2 -4
- package/src/bifrost/cache-headers.ts +8 -0
- package/src/bifrost/index.ts +1 -0
- package/src/create-runtime.ts +26 -1
- package/src/db/memory-store.helpers.ts +1 -3
- package/src/db/schema-fingerprint.ts +1 -3
- package/src/queues/document-processor.queue.ts +2 -4
- package/src/queues/post-chat-memory.queue.ts +8 -2
- package/src/queues/recent-activity-title-refinement.queue.ts +1 -1
- package/src/queues/skill-extraction.queue.ts +1 -1
- package/src/queues/workstream-title-generation.queue.ts +1 -1
- package/src/redis/redis-lease-lock.ts +1 -2
- package/src/runtime/agent-runtime-policy.ts +3 -14
- package/src/runtime/context-compaction.ts +2 -4
- package/src/runtime/index.ts +1 -1
- package/src/runtime/runtime-config.ts +86 -2
- package/src/runtime/runtime-extensions.ts +0 -1
- package/src/runtime/social-chat.ts +752 -0
- package/src/runtime/team-consultation-orchestrator.ts +0 -4
- package/src/services/agent-executor.service.ts +0 -1
- package/src/services/document-chunk.service.ts +1 -3
- package/src/services/index.ts +1 -0
- package/src/services/memory.service.ts +7 -2
- package/src/services/recent-activity.service.ts +1 -3
- package/src/services/social-chat-history.service.ts +197 -0
- package/src/services/workstream-message.service.ts +1 -3
- package/src/services/workstream-turn-preparation.service.ts +0 -23
- package/src/system-agents/context-compaction.agent.ts +2 -0
- package/src/system-agents/delegated-agent-factory.ts +3 -0
- package/src/system-agents/memory-reranker.agent.ts +4 -2
- package/src/system-agents/memory.agent.ts +2 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
- package/src/system-agents/skill-extractor.agent.ts +2 -0
- package/src/system-agents/skill-manager.agent.ts +2 -0
- package/src/system-agents/title-generator.agent.ts +2 -0
- package/src/tools/research-topic.tool.ts +2 -0
- package/src/tools/team-think.tool.ts +0 -3
- package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +43 -10
- package/src/workers/skill-extraction.runner.ts +25 -5
- package/src/workers/utils/repo-structure-extractor.ts +2 -2
- package/src/workers/utils/workstream-message-query.ts +3 -5
- package/src/runtime/workstream-routing-policy.ts +0 -267
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import { createSlackAdapter } from '@chat-adapter/slack'
|
|
2
|
+
import { createIoRedisState } from '@chat-adapter/state-ioredis'
|
|
3
|
+
import {
|
|
4
|
+
buildSlackSocialReplyMarkdown,
|
|
5
|
+
CONSULT_SPECIALIST_TOOL_NAME,
|
|
6
|
+
ConsultSpecialistArgsSchema,
|
|
7
|
+
stripSlackToolExecutionNoticeMarkdown,
|
|
8
|
+
} from '@lota-sdk/shared'
|
|
9
|
+
import type { ChatMessage, ConsultSpecialistArgs } from '@lota-sdk/shared'
|
|
10
|
+
import { stepCountIs, tool as createTool } from 'ai'
|
|
11
|
+
import type { ToolLoopAgent, ToolSet } from 'ai'
|
|
12
|
+
import { Chat, ConsoleLogger } from 'chat'
|
|
13
|
+
import type { Message, Thread, WebhookOptions } from 'chat'
|
|
14
|
+
import type IORedis from 'ioredis'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
agentDisplayNames,
|
|
18
|
+
createAgent,
|
|
19
|
+
getAgentRuntimeConfig,
|
|
20
|
+
teamConsultParticipants,
|
|
21
|
+
} from '../config/agent-defaults'
|
|
22
|
+
import { aiLogger } from '../config/logger'
|
|
23
|
+
import { recordIdToString } from '../db/record-id'
|
|
24
|
+
import { TABLES } from '../db/tables'
|
|
25
|
+
import { enqueuePostChatMemory } from '../queues/post-chat-memory.queue'
|
|
26
|
+
import { enqueueRegularChatMemoryDigest } from '../queues/regular-chat-memory-digest.queue'
|
|
27
|
+
import { enqueueSkillExtraction } from '../queues/skill-extraction.queue'
|
|
28
|
+
import type {
|
|
29
|
+
BuildSocialChatAgentToolsParams,
|
|
30
|
+
LotaRuntimeSocialChatConfig,
|
|
31
|
+
LotaSocialChatResolvedContext,
|
|
32
|
+
} from '../runtime/runtime-config'
|
|
33
|
+
import { getRuntimeAdapters } from '../runtime/runtime-extensions'
|
|
34
|
+
import { learnedSkillService } from '../services/learned-skill.service'
|
|
35
|
+
import { memoryService } from '../services/memory.service'
|
|
36
|
+
import { socialChatHistoryService } from '../services/social-chat-history.service'
|
|
37
|
+
import { safeEnqueue } from '../utils/async'
|
|
38
|
+
import { buildAgentPromptContext } from './agent-prompt-context'
|
|
39
|
+
import { createServerRunAbortController } from './agent-stream-helpers'
|
|
40
|
+
import { buildAgentHistoryMessages, extractMessageText, toHistoryMessages } from './workstream-chat-helpers'
|
|
41
|
+
|
|
42
|
+
const DEFAULT_SOCIAL_CHAT_AGENT_ID = 'socialChat'
|
|
43
|
+
const DEFAULT_SOCIAL_CHAT_AGENT_DISPLAY_NAME = 'Lota'
|
|
44
|
+
const DEFAULT_SOCIAL_CHAT_STATE_PREFIX = 'lota:social:chat-sdk'
|
|
45
|
+
const DEFAULT_SOCIAL_CHAT_DEDUPE_TTL_MS = 15 * 60 * 1000
|
|
46
|
+
const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
|
|
47
|
+
|
|
48
|
+
type SocialChatAgent = ToolLoopAgent<never, ToolSet>
|
|
49
|
+
|
|
50
|
+
export interface LotaRuntimeSocialChat {
|
|
51
|
+
enabled: boolean
|
|
52
|
+
initialize(): Promise<void>
|
|
53
|
+
shutdown(): Promise<void>
|
|
54
|
+
webhooks: { slack(request: Request, options?: WebhookOptions): Promise<Response> }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface SlackSocialMessageContext {
|
|
58
|
+
channelId: string
|
|
59
|
+
threadId: string
|
|
60
|
+
messageId: string
|
|
61
|
+
text: string
|
|
62
|
+
authorId?: string
|
|
63
|
+
authorName?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createDisabledSocialChatRuntime(): LotaRuntimeSocialChat {
|
|
67
|
+
return {
|
|
68
|
+
enabled: false,
|
|
69
|
+
async initialize() {},
|
|
70
|
+
async shutdown() {},
|
|
71
|
+
webhooks: {
|
|
72
|
+
async slack() {
|
|
73
|
+
return new Response('Social chat is disabled.', { status: 404 })
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toOptionalTrimmedString(value: unknown): string | undefined {
|
|
80
|
+
if (typeof value !== 'string') return undefined
|
|
81
|
+
const normalized = value.trim()
|
|
82
|
+
return normalized.length > 0 ? normalized : undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readSlackAuthorName(message: Message): string | undefined {
|
|
86
|
+
return (
|
|
87
|
+
toOptionalTrimmedString(message.author.fullName) ??
|
|
88
|
+
toOptionalTrimmedString(message.author.userName) ??
|
|
89
|
+
toOptionalTrimmedString(message.author.userId)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildSocialChatIdentitySection(agentDisplayName: string): string {
|
|
94
|
+
return [
|
|
95
|
+
'<social-chat-agent>',
|
|
96
|
+
`You are ${agentDisplayName}, the social chat agent for this workspace.`,
|
|
97
|
+
'- You respond inside Slack threads.',
|
|
98
|
+
'- Use the available tools, memories, learned skills, and workspace knowledge before guessing.',
|
|
99
|
+
'- If the answer may depend on prior workspace decisions, preferences, promises, history, or relationship context, check memory with the available memory tools before answering.',
|
|
100
|
+
'- If the user shares a URL or asks about a website/page, read it with fetchWebpage before answering. Do not summarize website contents from prior knowledge.',
|
|
101
|
+
'- When another roster agent is better suited for a question, consult that specialist and then synthesize the final answer yourself.',
|
|
102
|
+
'- Keep the final reply crisp and readable for Slack unless the user explicitly asks for depth.',
|
|
103
|
+
'</social-chat-agent>',
|
|
104
|
+
].join('\n')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildThreadTranscript(
|
|
108
|
+
messages: Array<{
|
|
109
|
+
role: 'user' | 'assistant'
|
|
110
|
+
parts: Array<Record<string, unknown>>
|
|
111
|
+
metadata?: Record<string, unknown>
|
|
112
|
+
}>,
|
|
113
|
+
): string {
|
|
114
|
+
const historyMessages = toHistoryMessages(messages)
|
|
115
|
+
if (historyMessages.length === 0) return 'No prior thread history.'
|
|
116
|
+
|
|
117
|
+
return historyMessages
|
|
118
|
+
.map((message) =>
|
|
119
|
+
message.role === 'user' ? `User: ${message.content}` : `${message.agentName ?? 'Assistant'}: ${message.content}`,
|
|
120
|
+
)
|
|
121
|
+
.join('\n\n')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildLeadAgentPrompt(params: {
|
|
125
|
+
agentDisplayName: string
|
|
126
|
+
channelId: string
|
|
127
|
+
threadId: string
|
|
128
|
+
transcript: string
|
|
129
|
+
latestUserMessage: string
|
|
130
|
+
latestAuthorName?: string
|
|
131
|
+
}): string {
|
|
132
|
+
return [
|
|
133
|
+
`Platform: Slack`,
|
|
134
|
+
`Channel ID: ${params.channelId}`,
|
|
135
|
+
`Thread ID: ${params.threadId}`,
|
|
136
|
+
`Current Slack author: ${params.latestAuthorName ?? 'Unknown'}`,
|
|
137
|
+
'',
|
|
138
|
+
`You are replying as ${params.agentDisplayName}.`,
|
|
139
|
+
'',
|
|
140
|
+
'<thread-transcript>',
|
|
141
|
+
params.transcript,
|
|
142
|
+
'</thread-transcript>',
|
|
143
|
+
'',
|
|
144
|
+
'Latest user message:',
|
|
145
|
+
params.latestUserMessage,
|
|
146
|
+
].join('\n')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildSpecialistPrompt(params: {
|
|
150
|
+
requesterName: string
|
|
151
|
+
agentName: string
|
|
152
|
+
task: string
|
|
153
|
+
transcript: string
|
|
154
|
+
}): string {
|
|
155
|
+
return [
|
|
156
|
+
`${params.requesterName} needs specialist help from ${params.agentName}.`,
|
|
157
|
+
'',
|
|
158
|
+
'<thread-transcript>',
|
|
159
|
+
params.transcript,
|
|
160
|
+
'</thread-transcript>',
|
|
161
|
+
'',
|
|
162
|
+
'Specialist task:',
|
|
163
|
+
params.task,
|
|
164
|
+
'',
|
|
165
|
+
'Answer only with the specialist guidance that should be used in the final Slack reply.',
|
|
166
|
+
].join('\n')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createAssistantMessage(params: { agentId: string; agentName: string; text: string }): ChatMessage {
|
|
170
|
+
return {
|
|
171
|
+
id: Bun.randomUUIDv7(),
|
|
172
|
+
role: 'assistant',
|
|
173
|
+
parts: [{ type: 'text', text: params.text }],
|
|
174
|
+
metadata: { agentId: params.agentId, agentName: params.agentName, createdAt: Date.now() },
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function createCursorId(message: { workspaceId: string; threadId: string; messageId: string }): string {
|
|
179
|
+
return `social:slack:${message.workspaceId}:${message.threadId}:${message.messageId}`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function toSafeJobIdSegment(value: string): string {
|
|
183
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createSocialMemoryDedupeKey(params: { workspaceId: string; threadId: string; messageId: string }): string {
|
|
187
|
+
return [
|
|
188
|
+
'social-memory',
|
|
189
|
+
toSafeJobIdSegment(params.workspaceId),
|
|
190
|
+
toSafeJobIdSegment(params.threadId),
|
|
191
|
+
toSafeJobIdSegment(params.messageId),
|
|
192
|
+
].join('-')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface ExecutableToolDefinition {
|
|
196
|
+
execute: (...args: unknown[]) => Promise<unknown>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function withLoggedToolSet(
|
|
200
|
+
tools: ToolSet,
|
|
201
|
+
params: { agentId: string; channelId: string; threadId: string; executedToolNames: string[] },
|
|
202
|
+
): ToolSet {
|
|
203
|
+
return Object.fromEntries(
|
|
204
|
+
Object.entries(tools).map(([toolName, toolDefinition]) => {
|
|
205
|
+
if (
|
|
206
|
+
typeof toolDefinition !== 'object' ||
|
|
207
|
+
!('execute' in toolDefinition) ||
|
|
208
|
+
typeof toolDefinition.execute !== 'function'
|
|
209
|
+
) {
|
|
210
|
+
return [toolName, toolDefinition]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const executableTool = toolDefinition as typeof toolDefinition & ExecutableToolDefinition
|
|
214
|
+
|
|
215
|
+
return [
|
|
216
|
+
toolName,
|
|
217
|
+
{
|
|
218
|
+
...toolDefinition,
|
|
219
|
+
execute: async (...args: unknown[]): Promise<unknown> => {
|
|
220
|
+
aiLogger.info`Slack social-chat tool start: agentId=${params.agentId}, tool=${toolName}, channelId=${params.channelId}, threadId=${params.threadId}`
|
|
221
|
+
try {
|
|
222
|
+
const result: unknown = await executableTool.execute(...args)
|
|
223
|
+
params.executedToolNames.push(toolName)
|
|
224
|
+
aiLogger.info`Slack social-chat tool finish: agentId=${params.agentId}, tool=${toolName}, channelId=${params.channelId}, threadId=${params.threadId}`
|
|
225
|
+
return result
|
|
226
|
+
} catch (error) {
|
|
227
|
+
aiLogger.warn`Slack social-chat tool failed: agentId=${params.agentId}, tool=${toolName}, channelId=${params.channelId}, threadId=${params.threadId}, error=${error}`
|
|
228
|
+
throw error
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
]
|
|
233
|
+
}),
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeSocialHistoryMessage(params: {
|
|
238
|
+
workspaceId: string
|
|
239
|
+
channelId: string
|
|
240
|
+
agentId: string
|
|
241
|
+
agentDisplayName: string
|
|
242
|
+
message: Message
|
|
243
|
+
textOverride?: string
|
|
244
|
+
}): {
|
|
245
|
+
source: 'social'
|
|
246
|
+
sourceId: string
|
|
247
|
+
platform: 'slack'
|
|
248
|
+
workspaceId: string
|
|
249
|
+
channelId: string
|
|
250
|
+
threadId: string
|
|
251
|
+
messageId: string
|
|
252
|
+
role: 'user' | 'assistant'
|
|
253
|
+
parts: Array<Record<string, unknown>>
|
|
254
|
+
metadata?: Record<string, unknown>
|
|
255
|
+
cursor: { createdAt: Date; id: string }
|
|
256
|
+
} | null {
|
|
257
|
+
const role: 'user' | 'assistant' = params.message.author.isMe ? 'assistant' : 'user'
|
|
258
|
+
const text = toOptionalTrimmedString(
|
|
259
|
+
role === 'assistant'
|
|
260
|
+
? stripSlackToolExecutionNoticeMarkdown(params.textOverride ?? params.message.text)
|
|
261
|
+
: (params.textOverride ?? params.message.text),
|
|
262
|
+
)
|
|
263
|
+
const fileParts = params.message.attachments.map((attachment) => ({
|
|
264
|
+
type: 'file',
|
|
265
|
+
filename: attachment.name ?? 'attachment',
|
|
266
|
+
mediaType: attachment.mimeType ?? 'application/octet-stream',
|
|
267
|
+
sizeBytes: typeof attachment.size === 'number' ? attachment.size : null,
|
|
268
|
+
storageKey: attachment.url ?? 'external',
|
|
269
|
+
}))
|
|
270
|
+
const parts = [...(text ? [{ type: 'text', text }] : []), ...fileParts]
|
|
271
|
+
if (parts.length === 0) return null
|
|
272
|
+
|
|
273
|
+
const createdAt = params.message.metadata.dateSent
|
|
274
|
+
const metadata: Record<string, unknown> = {
|
|
275
|
+
platform: 'slack',
|
|
276
|
+
channelId: params.channelId,
|
|
277
|
+
threadId: params.message.threadId,
|
|
278
|
+
messageId: params.message.id,
|
|
279
|
+
authorId: params.message.author.userId,
|
|
280
|
+
authorName: readSlackAuthorName(params.message),
|
|
281
|
+
}
|
|
282
|
+
if (role === 'assistant') {
|
|
283
|
+
metadata.agentId = params.agentId
|
|
284
|
+
metadata.agentName = params.agentDisplayName
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
source: 'social',
|
|
289
|
+
sourceId: `slack:${params.channelId}:${params.message.threadId}`,
|
|
290
|
+
platform: 'slack',
|
|
291
|
+
workspaceId: params.workspaceId,
|
|
292
|
+
channelId: params.channelId,
|
|
293
|
+
threadId: params.message.threadId,
|
|
294
|
+
messageId: params.message.id,
|
|
295
|
+
role,
|
|
296
|
+
parts,
|
|
297
|
+
metadata,
|
|
298
|
+
cursor: {
|
|
299
|
+
createdAt,
|
|
300
|
+
id: createCursorId({
|
|
301
|
+
workspaceId: params.workspaceId,
|
|
302
|
+
threadId: params.message.threadId,
|
|
303
|
+
messageId: params.message.id,
|
|
304
|
+
}),
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function collectThreadMessages(thread: Thread, incomingMessage: Message): Promise<Message[]> {
|
|
310
|
+
try {
|
|
311
|
+
const messages: Message[] = []
|
|
312
|
+
for await (const message of thread.allMessages) {
|
|
313
|
+
messages.push(message)
|
|
314
|
+
}
|
|
315
|
+
return messages.length > 0 ? messages : [incomingMessage]
|
|
316
|
+
} catch {
|
|
317
|
+
return thread.recentMessages.length > 0 ? [...thread.recentMessages] : [incomingMessage]
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildBuildToolsParams(params: {
|
|
322
|
+
agentId: string
|
|
323
|
+
context: LotaSocialChatResolvedContext
|
|
324
|
+
workspaceIdString: string
|
|
325
|
+
userIdString: string
|
|
326
|
+
messageContext: SlackSocialMessageContext
|
|
327
|
+
memoryBlock: string
|
|
328
|
+
onAppendMemoryBlock: (value: string) => void
|
|
329
|
+
}): BuildSocialChatAgentToolsParams {
|
|
330
|
+
return {
|
|
331
|
+
agentId: params.agentId,
|
|
332
|
+
workspaceId: params.context.workspaceId,
|
|
333
|
+
workspaceIdString: params.workspaceIdString,
|
|
334
|
+
userId: params.context.userId,
|
|
335
|
+
userIdString: params.userIdString,
|
|
336
|
+
userName: params.context.userName,
|
|
337
|
+
platform: 'slack',
|
|
338
|
+
channelId: params.messageContext.channelId,
|
|
339
|
+
threadId: params.messageContext.threadId,
|
|
340
|
+
incomingMessageId: params.messageContext.messageId,
|
|
341
|
+
incomingText: params.messageContext.text,
|
|
342
|
+
memoryBlock: params.memoryBlock,
|
|
343
|
+
onAppendMemoryBlock: params.onAppendMemoryBlock,
|
|
344
|
+
context: null,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function createSocialChatRuntime(params: {
|
|
349
|
+
redisClient: IORedis
|
|
350
|
+
socialChat?: LotaRuntimeSocialChatConfig
|
|
351
|
+
}): LotaRuntimeSocialChat {
|
|
352
|
+
const socialChatConfig = params.socialChat
|
|
353
|
+
const slackConfig = socialChatConfig?.slack
|
|
354
|
+
const slackEnabled = Boolean(slackConfig?.botToken?.trim() && slackConfig.signingSecret?.trim())
|
|
355
|
+
|
|
356
|
+
if (!socialChatConfig || !slackEnabled) {
|
|
357
|
+
return createDisabledSocialChatRuntime()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const socialAgentId = toOptionalTrimmedString(socialChatConfig.agentId) ?? DEFAULT_SOCIAL_CHAT_AGENT_ID
|
|
361
|
+
const socialAgentDisplayName =
|
|
362
|
+
toOptionalTrimmedString(socialChatConfig.agentDisplayName) ?? DEFAULT_SOCIAL_CHAT_AGENT_DISPLAY_NAME
|
|
363
|
+
const stateKeyPrefix =
|
|
364
|
+
toOptionalTrimmedString(socialChatConfig.stateRedisKeyPrefix) ?? DEFAULT_SOCIAL_CHAT_STATE_PREFIX
|
|
365
|
+
const chat = new Chat({
|
|
366
|
+
userName: toOptionalTrimmedString(slackConfig?.userName) ?? socialAgentDisplayName,
|
|
367
|
+
adapters: {
|
|
368
|
+
slack: createSlackAdapter({
|
|
369
|
+
botToken: slackConfig?.botToken,
|
|
370
|
+
signingSecret: slackConfig?.signingSecret,
|
|
371
|
+
userName: toOptionalTrimmedString(slackConfig?.userName) ?? socialAgentDisplayName,
|
|
372
|
+
}),
|
|
373
|
+
},
|
|
374
|
+
state: createIoRedisState({
|
|
375
|
+
client: params.redisClient,
|
|
376
|
+
keyPrefix: stateKeyPrefix,
|
|
377
|
+
logger: new ConsoleLogger('warn', 'lota-social-chat'),
|
|
378
|
+
}),
|
|
379
|
+
dedupeTtlMs: slackConfig?.dedupeTtlMs ?? DEFAULT_SOCIAL_CHAT_DEDUPE_TTL_MS,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const initialize = async () => {
|
|
383
|
+
await chat.initialize()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const shutdown = async () => {
|
|
387
|
+
await chat.shutdown()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const handleMessage = async (thread: Thread, incomingMessage: Message) => {
|
|
391
|
+
const rawSlackMessage = incomingMessage.raw as { channel?: unknown } | undefined
|
|
392
|
+
const channelId = toOptionalTrimmedString(rawSlackMessage?.channel) ?? thread.channelId
|
|
393
|
+
const messageContext: SlackSocialMessageContext = {
|
|
394
|
+
channelId,
|
|
395
|
+
threadId: thread.id,
|
|
396
|
+
messageId: incomingMessage.id,
|
|
397
|
+
text: incomingMessage.text.trim(),
|
|
398
|
+
authorId: toOptionalTrimmedString(incomingMessage.author.userId),
|
|
399
|
+
authorName: readSlackAuthorName(incomingMessage),
|
|
400
|
+
}
|
|
401
|
+
aiLogger.info`Slack social-chat message received: channelId=${messageContext.channelId}, threadId=${messageContext.threadId}, messageId=${messageContext.messageId}, author=${messageContext.authorName ?? 'unknown'}, textLength=${messageContext.text.length}`
|
|
402
|
+
|
|
403
|
+
const resolvedContext = await socialChatConfig.resolveContext({
|
|
404
|
+
platform: 'slack',
|
|
405
|
+
channelId: messageContext.channelId,
|
|
406
|
+
threadId: messageContext.threadId,
|
|
407
|
+
messageId: messageContext.messageId,
|
|
408
|
+
text: messageContext.text,
|
|
409
|
+
authorId: messageContext.authorId,
|
|
410
|
+
authorName: messageContext.authorName,
|
|
411
|
+
})
|
|
412
|
+
const workspaceIdString = recordIdToString(resolvedContext.workspaceId, TABLES.ORGANIZATION)
|
|
413
|
+
const userIdString = recordIdToString(resolvedContext.userId, TABLES.USER)
|
|
414
|
+
aiLogger.info`Slack social-chat context resolved: workspaceId=${workspaceIdString}, userId=${userIdString}`
|
|
415
|
+
|
|
416
|
+
const threadMessages = await collectThreadMessages(thread, incomingMessage)
|
|
417
|
+
const normalizedMessages = threadMessages
|
|
418
|
+
.map((message) =>
|
|
419
|
+
normalizeSocialHistoryMessage({
|
|
420
|
+
workspaceId: workspaceIdString,
|
|
421
|
+
channelId: messageContext.channelId,
|
|
422
|
+
agentId: socialAgentId,
|
|
423
|
+
agentDisplayName: socialAgentDisplayName,
|
|
424
|
+
message,
|
|
425
|
+
}),
|
|
426
|
+
)
|
|
427
|
+
.filter((message): message is NonNullable<typeof message> => message !== null)
|
|
428
|
+
await socialChatHistoryService.upsertMessages(normalizedMessages)
|
|
429
|
+
|
|
430
|
+
const historyBeforeReply = await socialChatHistoryService.listThreadMessages({
|
|
431
|
+
workspaceId: workspaceIdString,
|
|
432
|
+
threadId: messageContext.threadId,
|
|
433
|
+
})
|
|
434
|
+
const currentUserMessage =
|
|
435
|
+
historyBeforeReply.find((message) => message.messageId === incomingMessage.id) ?? historyBeforeReply.at(-1)
|
|
436
|
+
const priorHistory = currentUserMessage
|
|
437
|
+
? historyBeforeReply.filter((message) => message.cursor.id !== currentUserMessage.cursor.id)
|
|
438
|
+
: historyBeforeReply
|
|
439
|
+
|
|
440
|
+
const workspaceProvider = getRuntimeAdapters().services?.workspaceProvider
|
|
441
|
+
const workspace = workspaceProvider ? await workspaceProvider.getWorkspace(resolvedContext.workspaceId) : {}
|
|
442
|
+
const lifecycleState = workspaceProvider ? await workspaceProvider.getLifecycleState?.(workspace) : undefined
|
|
443
|
+
const workspaceProfileState = workspaceProvider
|
|
444
|
+
? await workspaceProvider.readProfileProjectionState?.(workspace)
|
|
445
|
+
: undefined
|
|
446
|
+
const [recentDomainEvents, promptSummary] = await Promise.all([
|
|
447
|
+
workspaceProvider?.listRecentDomainEvents?.(resolvedContext.workspaceId, 5) ??
|
|
448
|
+
Promise.resolve([] as Array<Record<string, unknown>>),
|
|
449
|
+
workspaceProvider?.buildPromptSummary
|
|
450
|
+
? workspaceProvider.buildPromptSummary(resolvedContext.workspaceId).catch(() => undefined)
|
|
451
|
+
: Promise.resolve(undefined),
|
|
452
|
+
])
|
|
453
|
+
|
|
454
|
+
const promptContext = buildAgentPromptContext({
|
|
455
|
+
workspaceName:
|
|
456
|
+
workspaceProfileState?.workspaceName ?? toOptionalTrimmedString((workspace as { name?: unknown }).name),
|
|
457
|
+
summaryBlock: workspaceProfileState?.summaryBlock,
|
|
458
|
+
structuredProfile: workspaceProfileState?.structuredProfile,
|
|
459
|
+
promptSummary,
|
|
460
|
+
userName: messageContext.authorName,
|
|
461
|
+
recentDomainEvents,
|
|
462
|
+
})
|
|
463
|
+
const retrievedKnowledgeSection =
|
|
464
|
+
lifecycleState?.bootstrapActive || messageContext.text.length === 0
|
|
465
|
+
? undefined
|
|
466
|
+
: await workspaceProvider?.buildRetrievedKnowledgeSection?.({
|
|
467
|
+
workspaceId: workspaceIdString,
|
|
468
|
+
userId: userIdString,
|
|
469
|
+
query: messageContext.text,
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
const [preSeededMemoriesSection, learnedSkillsSection] = await Promise.all([
|
|
473
|
+
memoryService.getTopMemories({
|
|
474
|
+
orgId: workspaceIdString,
|
|
475
|
+
agentName: socialAgentId,
|
|
476
|
+
limit: PRESEEDED_MEMORY_LOOKUP_LIMIT,
|
|
477
|
+
}),
|
|
478
|
+
lifecycleState?.bootstrapActive
|
|
479
|
+
? Promise.resolve(undefined)
|
|
480
|
+
: learnedSkillService
|
|
481
|
+
.retrieveForTurn({
|
|
482
|
+
orgId: workspaceIdString,
|
|
483
|
+
agentId: socialAgentId,
|
|
484
|
+
query: messageContext.text,
|
|
485
|
+
limit: 3,
|
|
486
|
+
minConfidence: 0.6,
|
|
487
|
+
})
|
|
488
|
+
.catch((error) => {
|
|
489
|
+
aiLogger.warn`Failed to retrieve learned skills for ${socialAgentId}: ${error}`
|
|
490
|
+
return undefined
|
|
491
|
+
}),
|
|
492
|
+
])
|
|
493
|
+
|
|
494
|
+
let memoryBlock = ''
|
|
495
|
+
const runtimeConfig = getAgentRuntimeConfig({
|
|
496
|
+
agentId: socialAgentId,
|
|
497
|
+
workstreamMode: 'group',
|
|
498
|
+
mode: 'workstreamMode',
|
|
499
|
+
onboardingActive: lifecycleState?.bootstrapActive ?? false,
|
|
500
|
+
linearInstalled: false,
|
|
501
|
+
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
502
|
+
preSeededMemoriesSection,
|
|
503
|
+
retrievedKnowledgeSection,
|
|
504
|
+
learnedSkillsSection,
|
|
505
|
+
additionalInstructionSections: [buildSocialChatIdentitySection(socialAgentDisplayName)],
|
|
506
|
+
}) as Record<string, unknown>
|
|
507
|
+
const agentFactory = createAgent[runtimeConfig.id as string]
|
|
508
|
+
if (typeof agentFactory !== 'function') {
|
|
509
|
+
throw new Error(`Social chat agent factory is not configured for ${String(runtimeConfig.id)}`)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const consultParticipants = [
|
|
513
|
+
...new Set(
|
|
514
|
+
(await socialChatConfig.getConsultParticipants?.({
|
|
515
|
+
workspaceId: resolvedContext.workspaceId,
|
|
516
|
+
workspaceIdString,
|
|
517
|
+
platform: 'slack',
|
|
518
|
+
})) ?? [...teamConsultParticipants],
|
|
519
|
+
),
|
|
520
|
+
].filter((agentId) => agentId !== socialAgentId)
|
|
521
|
+
const executedToolNames: string[] = []
|
|
522
|
+
|
|
523
|
+
const baseTools = withLoggedToolSet(
|
|
524
|
+
await socialChatConfig.buildAgentTools(
|
|
525
|
+
buildBuildToolsParams({
|
|
526
|
+
agentId: socialAgentId,
|
|
527
|
+
context: resolvedContext,
|
|
528
|
+
workspaceIdString,
|
|
529
|
+
userIdString,
|
|
530
|
+
messageContext,
|
|
531
|
+
memoryBlock,
|
|
532
|
+
onAppendMemoryBlock: (value: string) => {
|
|
533
|
+
memoryBlock = value
|
|
534
|
+
},
|
|
535
|
+
}),
|
|
536
|
+
),
|
|
537
|
+
{
|
|
538
|
+
agentId: socialAgentId,
|
|
539
|
+
channelId: messageContext.channelId,
|
|
540
|
+
threadId: messageContext.threadId,
|
|
541
|
+
executedToolNames,
|
|
542
|
+
},
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
const transcript = buildThreadTranscript(historyBeforeReply)
|
|
546
|
+
const runAbort = createServerRunAbortController()
|
|
547
|
+
|
|
548
|
+
const consultSpecialistTool = createTool({
|
|
549
|
+
description: 'Consult one specialist teammate for targeted guidance before replying to the user.',
|
|
550
|
+
inputSchema: ConsultSpecialistArgsSchema,
|
|
551
|
+
execute: async ({ agentId, task }: ConsultSpecialistArgs) => {
|
|
552
|
+
if (!consultParticipants.includes(agentId)) {
|
|
553
|
+
throw new Error(`Agent "${agentId}" is not an allowed social-chat specialist.`)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
let specialistMemoryBlock = ''
|
|
557
|
+
const specialistTools = withLoggedToolSet(
|
|
558
|
+
await socialChatConfig.buildAgentTools(
|
|
559
|
+
buildBuildToolsParams({
|
|
560
|
+
agentId,
|
|
561
|
+
context: resolvedContext,
|
|
562
|
+
workspaceIdString,
|
|
563
|
+
userIdString,
|
|
564
|
+
messageContext,
|
|
565
|
+
memoryBlock: specialistMemoryBlock,
|
|
566
|
+
onAppendMemoryBlock: (value: string) => {
|
|
567
|
+
specialistMemoryBlock = value
|
|
568
|
+
},
|
|
569
|
+
}),
|
|
570
|
+
),
|
|
571
|
+
{ agentId, channelId: messageContext.channelId, threadId: messageContext.threadId, executedToolNames },
|
|
572
|
+
)
|
|
573
|
+
const [specialistPreSeededMemories, specialistLearnedSkills] = await Promise.all([
|
|
574
|
+
memoryService.getTopMemories({
|
|
575
|
+
orgId: workspaceIdString,
|
|
576
|
+
agentName: agentId,
|
|
577
|
+
limit: PRESEEDED_MEMORY_LOOKUP_LIMIT,
|
|
578
|
+
}),
|
|
579
|
+
lifecycleState?.bootstrapActive
|
|
580
|
+
? Promise.resolve(undefined)
|
|
581
|
+
: learnedSkillService
|
|
582
|
+
.retrieveForTurn({ orgId: workspaceIdString, agentId, query: task, limit: 3, minConfidence: 0.6 })
|
|
583
|
+
.catch((error) => {
|
|
584
|
+
aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
|
|
585
|
+
return undefined
|
|
586
|
+
}),
|
|
587
|
+
])
|
|
588
|
+
|
|
589
|
+
const specialistRuntimeConfig = getAgentRuntimeConfig({
|
|
590
|
+
agentId,
|
|
591
|
+
workstreamMode: 'group',
|
|
592
|
+
mode: 'fixedWorkstreamMode',
|
|
593
|
+
onboardingActive: lifecycleState?.bootstrapActive ?? false,
|
|
594
|
+
linearInstalled: false,
|
|
595
|
+
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
596
|
+
preSeededMemoriesSection: specialistPreSeededMemories,
|
|
597
|
+
retrievedKnowledgeSection,
|
|
598
|
+
learnedSkillsSection: specialistLearnedSkills,
|
|
599
|
+
additionalInstructionSections: [
|
|
600
|
+
`You are supporting ${socialAgentDisplayName} in a Slack social-chat thread. Stay within your role.`,
|
|
601
|
+
],
|
|
602
|
+
}) as Record<string, unknown>
|
|
603
|
+
const specialistFactory = createAgent[specialistRuntimeConfig.id as string]
|
|
604
|
+
if (typeof specialistFactory !== 'function') {
|
|
605
|
+
throw new Error(`Social specialist agent factory is not configured for ${String(specialistRuntimeConfig.id)}`)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const specialistAgent = specialistFactory({
|
|
609
|
+
mode: 'fixedWorkstreamMode',
|
|
610
|
+
tools: specialistTools,
|
|
611
|
+
extraInstructions: specialistRuntimeConfig.extraInstructions,
|
|
612
|
+
stopWhen: [stepCountIs(specialistRuntimeConfig.maxSteps as number)],
|
|
613
|
+
}) as SocialChatAgent
|
|
614
|
+
const specialistResponse = await specialistAgent.generate({
|
|
615
|
+
prompt: buildSpecialistPrompt({
|
|
616
|
+
requesterName: socialAgentDisplayName,
|
|
617
|
+
agentName: agentDisplayNames[agentId] ?? agentId,
|
|
618
|
+
task,
|
|
619
|
+
transcript,
|
|
620
|
+
}),
|
|
621
|
+
abortSignal: runAbort.signal,
|
|
622
|
+
})
|
|
623
|
+
const text = specialistResponse.text.trim()
|
|
624
|
+
if (!text) {
|
|
625
|
+
throw new Error(`Specialist ${agentId} returned an empty response.`)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return createAssistantMessage({ agentId, agentName: agentDisplayNames[agentId] ?? agentId, text })
|
|
629
|
+
},
|
|
630
|
+
toModelOutput: ({ output }) => {
|
|
631
|
+
const message = output
|
|
632
|
+
const agentName =
|
|
633
|
+
typeof message.metadata?.agentName === 'string' && message.metadata.agentName.trim().length > 0
|
|
634
|
+
? message.metadata.agentName.trim()
|
|
635
|
+
: 'Specialist'
|
|
636
|
+
const summary = extractMessageText(message).trim()
|
|
637
|
+
return {
|
|
638
|
+
type: 'text',
|
|
639
|
+
value: summary ? `${agentName}: ${summary}` : `${agentName} completed the requested task.`,
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
const agent = agentFactory({
|
|
645
|
+
mode: 'workstreamMode',
|
|
646
|
+
tools: { ...baseTools, [CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool },
|
|
647
|
+
extraInstructions: runtimeConfig.extraInstructions,
|
|
648
|
+
stopWhen: [stepCountIs(runtimeConfig.maxSteps as number)],
|
|
649
|
+
}) as SocialChatAgent
|
|
650
|
+
|
|
651
|
+
await thread.startTyping('Thinking...').catch(() => undefined)
|
|
652
|
+
aiLogger.info`Slack social-chat generating reply: channelId=${messageContext.channelId}, threadId=${messageContext.threadId}`
|
|
653
|
+
const response = await agent.generate({
|
|
654
|
+
prompt: buildLeadAgentPrompt({
|
|
655
|
+
agentDisplayName: socialAgentDisplayName,
|
|
656
|
+
channelId: messageContext.channelId,
|
|
657
|
+
threadId: messageContext.threadId,
|
|
658
|
+
transcript,
|
|
659
|
+
latestUserMessage: messageContext.text,
|
|
660
|
+
latestAuthorName: messageContext.authorName,
|
|
661
|
+
}),
|
|
662
|
+
abortSignal: runAbort.signal,
|
|
663
|
+
})
|
|
664
|
+
const responseText = response.text.trim()
|
|
665
|
+
if (!responseText) {
|
|
666
|
+
throw new Error('Social chat agent returned an empty response.')
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const replyMarkdown = buildSlackSocialReplyMarkdown({ replyMarkdown: responseText, executedToolNames })
|
|
670
|
+
const sentMessage = await thread.post({ markdown: replyMarkdown })
|
|
671
|
+
aiLogger.info`Slack social-chat reply posted: channelId=${messageContext.channelId}, threadId=${messageContext.threadId}, replyMessageId=${sentMessage.id}`
|
|
672
|
+
const normalizedResponse = normalizeSocialHistoryMessage({
|
|
673
|
+
workspaceId: workspaceIdString,
|
|
674
|
+
channelId: messageContext.channelId,
|
|
675
|
+
agentId: socialAgentId,
|
|
676
|
+
agentDisplayName: socialAgentDisplayName,
|
|
677
|
+
message: sentMessage,
|
|
678
|
+
textOverride: responseText,
|
|
679
|
+
})
|
|
680
|
+
if (normalizedResponse) {
|
|
681
|
+
await socialChatHistoryService.upsertMessages([normalizedResponse])
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const priorHistoryMessages = toHistoryMessages(priorHistory)
|
|
685
|
+
const agentMessages = normalizedResponse ? buildAgentHistoryMessages([normalizedResponse]) : []
|
|
686
|
+
if (messageContext.text && agentMessages.length > 0) {
|
|
687
|
+
await safeEnqueue(
|
|
688
|
+
() =>
|
|
689
|
+
enqueuePostChatMemory(
|
|
690
|
+
{
|
|
691
|
+
orgId: workspaceIdString,
|
|
692
|
+
workstreamId: `social:slack:${messageContext.threadId}`,
|
|
693
|
+
sourceId: createCursorId({
|
|
694
|
+
workspaceId: workspaceIdString,
|
|
695
|
+
threadId: messageContext.threadId,
|
|
696
|
+
messageId: messageContext.messageId,
|
|
697
|
+
}),
|
|
698
|
+
userMessage: messageContext.text,
|
|
699
|
+
historyMessages: priorHistoryMessages,
|
|
700
|
+
agentMessages,
|
|
701
|
+
memoryBlock: memoryBlock.trim() ? memoryBlock : undefined,
|
|
702
|
+
source: 'social_chat',
|
|
703
|
+
sourceMetadata: {
|
|
704
|
+
platform: 'slack',
|
|
705
|
+
channelId: messageContext.channelId,
|
|
706
|
+
threadId: messageContext.threadId,
|
|
707
|
+
messageId: messageContext.messageId,
|
|
708
|
+
authorId: messageContext.authorId,
|
|
709
|
+
authorName: messageContext.authorName,
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
dedupeKey: createSocialMemoryDedupeKey({
|
|
714
|
+
workspaceId: workspaceIdString,
|
|
715
|
+
threadId: messageContext.threadId,
|
|
716
|
+
messageId: messageContext.messageId,
|
|
717
|
+
}),
|
|
718
|
+
},
|
|
719
|
+
),
|
|
720
|
+
{ operationName: 'social post-chat memory extraction enqueue' },
|
|
721
|
+
)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
await safeEnqueue(() => enqueueRegularChatMemoryDigest({ orgId: workspaceIdString }), {
|
|
725
|
+
operationName: 'social regular chat memory digest enqueue',
|
|
726
|
+
})
|
|
727
|
+
await safeEnqueue(() => enqueueSkillExtraction({ orgId: workspaceIdString }), {
|
|
728
|
+
operationName: 'social skill extraction enqueue',
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
chat.onNewMention(async (thread, message) => {
|
|
733
|
+
aiLogger.info`Slack social-chat new mention received: threadId=${thread.id}, messageId=${message.id}`
|
|
734
|
+
await thread.subscribe()
|
|
735
|
+
await handleMessage(thread, message)
|
|
736
|
+
})
|
|
737
|
+
chat.onSubscribedMessage(async (thread, message) => {
|
|
738
|
+
aiLogger.info`Slack social-chat subscribed thread message received: threadId=${thread.id}, messageId=${message.id}`
|
|
739
|
+
await handleMessage(thread, message)
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
enabled: true,
|
|
744
|
+
initialize,
|
|
745
|
+
shutdown,
|
|
746
|
+
webhooks: {
|
|
747
|
+
async slack(request: Request, options?: WebhookOptions) {
|
|
748
|
+
return chat.webhooks.slack(request, options)
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
}
|